1pub mod lifecycle;
2
3use std::collections::{BTreeMap, BTreeSet, VecDeque};
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6use std::fs::{File, OpenOptions};
7use std::io::{Seek, SeekFrom, Write};
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
10use std::sync::mpsc::{self, Receiver, SyncSender};
11use std::sync::Once;
12use std::sync::{Arc, Condvar, Mutex};
13use std::thread::{self, JoinHandle};
14use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
15
16use fathomdb_embedder_api::{Embedder, EmbedderError as RuntimeEmbedderError, EmbedderIdentity};
17use fathomdb_query::compile_text_query;
18use fathomdb_schema::{
19 migrate_with_event_sink, MigrationError as SchemaMigrationError, MigrationStepReport,
20 CANONICAL_TABLES, LOCK_SUFFIX, MIGRATIONS, SCHEMA_VERSION,
21};
22use jsonschema::JSONSchema;
23use rusqlite::{params, Connection};
24use serde_json::Value;
25use sha2::Digest;
26use sqlite_vec::sqlite3_vec_init;
27
28#[cfg(unix)]
29use std::os::unix::fs::OpenOptionsExt;
30
31const DEFAULT_EMBEDDER_NAME: &str = "fathomdb-noop";
32const DEFAULT_EMBEDDER_REVISION: &str = "0.6.0-scaffold";
33const DEFAULT_EMBEDDER_DIMENSION: u32 = 384;
34
35const DEFAULT_SLOW_THRESHOLD_MS: u64 = 100;
38const DEFAULT_VECTOR_PROFILE: &str = "default";
39const DEFAULT_VECTOR_PARTITION: &str = "vector_default";
40const REBUILD_DRAIN_TIMEOUT_MS: u64 = 30_000;
46const DEFAULT_PROVENANCE_ROW_CAP: u64 = 1_000_000;
47const PROJECTION_CURSOR_KEY: &str = "projection_cursor";
48const PROJECTION_WORKERS: usize = 2;
49const PROJECTION_INFLIGHT_LIMIT: usize = PROJECTION_WORKERS * 4;
50const PROJECTION_COMMIT_BATCH: usize = 16;
51const DEFAULT_PROJECTION_RETRY_DELAYS_MS: [u64; 3] = [1_000, 4_000, 16_000];
52
53const READER_POOL_SIZE: usize = 8;
57
58const READER_LOOKASIDE_SLOT_SIZE: std::os::raw::c_int = 1200;
66
67const READER_LOOKASIDE_SLOT_COUNT: std::os::raw::c_int = 500;
72
73pub struct Engine {
74 path: PathBuf,
75 next_cursor: AtomicU64,
76 closed: AtomicBool,
77 lock: Mutex<Option<File>>,
78 connection: Mutex<Option<Connection>>,
79 reader_pool: ReaderWorkerPool,
80 counters: lifecycle::Counters,
81 subscribers: Arc<lifecycle::SubscriberRegistry>,
82 profiling_enabled: Arc<AtomicBool>,
83 slow_threshold_ms: Arc<AtomicU64>,
84 runtime_embedder: Option<Arc<dyn Embedder>>,
85 runtime_embedder_identity: EmbedderIdentity,
86 projection_runtime: ProjectionRuntime,
87 provenance_row_cap: AtomicU64,
88 #[allow(clippy::vec_box)]
100 profile_contexts: Mutex<Vec<Box<ProfileContext>>>,
101 #[allow(dead_code)]
108 reader_lookaside_rcs: Vec<i32>,
109 #[cfg(debug_assertions)]
110 force_next_commit_failure: AtomicBool,
111}
112
113#[derive(Clone, Debug)]
114struct ProjectionJob {
115 cursor: u64,
116 kind: String,
117 body: String,
118}
119
120#[derive(Debug, Default)]
121struct ProjectionRuntimeState {
122 active_jobs: usize,
123 queued_jobs: usize,
124 frozen: bool,
125 pending_scan: bool,
126 stopping: bool,
127 in_flight: BTreeSet<u64>,
128}
129
130struct ProjectionRuntimeShared {
131 path: PathBuf,
132 embedder: Option<Arc<dyn Embedder>>,
133 embedder_identity: EmbedderIdentity,
134 state: Mutex<ProjectionRuntimeState>,
135 state_cvar: Condvar,
136 queue: Mutex<VecDeque<ProjectionJob>>,
137 queue_cvar: Condvar,
138 retry_delays_ms: Mutex<Vec<u64>>,
139}
140
141impl std::fmt::Debug for ProjectionRuntimeShared {
142 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
143 f.debug_struct("ProjectionRuntimeShared")
144 .field("path", &self.path)
145 .field("embedder_identity", &self.embedder_identity)
146 .finish_non_exhaustive()
147 }
148}
149
150#[derive(Debug)]
151struct ProjectionRuntime {
152 shared: Arc<ProjectionRuntimeShared>,
153 dispatcher: Mutex<Option<JoinHandle<()>>>,
154 workers: Mutex<Vec<JoinHandle<()>>>,
155}
156
157impl std::fmt::Debug for Engine {
158 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
159 f.debug_struct("Engine")
160 .field("path", &self.path)
161 .field("closed", &self.closed.load(Ordering::SeqCst))
162 .field("runtime_embedder_identity", &self.runtime_embedder_identity)
163 .finish_non_exhaustive()
164 }
165}
166
167#[derive(Debug)]
176struct ProfileContext {
177 subscribers: Arc<lifecycle::SubscriberRegistry>,
178 profiling_enabled: Arc<AtomicBool>,
179 slow_threshold_ms: Arc<AtomicU64>,
180}
181
182struct ReaderWorkerPool {
191 senders: Vec<SyncSender<ReaderRequest>>,
192 handles: Mutex<Option<Vec<JoinHandle<()>>>>,
193 next: AtomicUsize,
194 shutdown: AtomicBool,
195 live_workers: Arc<AtomicUsize>,
196}
197
198impl std::fmt::Debug for ReaderWorkerPool {
199 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
200 f.debug_struct("ReaderWorkerPool")
201 .field("worker_count", &self.senders.len())
202 .field("live_workers", &self.live_workers.load(Ordering::Relaxed))
203 .field("shutdown", &self.shutdown.load(Ordering::Relaxed))
204 .finish()
205 }
206}
207
208enum ReaderRequest {
212 Search {
213 compiled: fathomdb_query::CompiledQuery,
214 query_vector: Option<String>,
215 respond: SyncSender<ReaderResponse>,
216 },
217 Shutdown,
218 #[cfg(debug_assertions)]
224 LookasideStatus {
225 respond: SyncSender<i32>,
226 },
227 #[cfg(debug_assertions)]
233 CacheStatus {
234 snapshot_label: String,
235 respond: SyncSender<(String, i32, i32, i32)>,
236 },
237}
238
239type ReaderResponse = rusqlite::Result<(u64, Option<SoftFallback>, Vec<String>)>;
240
241#[cfg(debug_assertions)]
245#[doc(hidden)]
246#[derive(Clone, Debug)]
247pub struct CacheStatusReply {
248 pub worker_idx: usize,
249 pub snapshot_label: String,
250 pub cache_hit: i32,
251 pub cache_miss: i32,
252 pub cache_used_bytes: i32,
253}
254
255const READER_WORKER_CHANNEL_CAPACITY: usize = 4;
259
260impl ReaderWorkerPool {
261 fn new(connections: Vec<Connection>) -> Self {
262 let live_workers = Arc::new(AtomicUsize::new(0));
263 let mut senders = Vec::with_capacity(connections.len());
264 let mut handles = Vec::with_capacity(connections.len());
265 for (idx, connection) in connections.into_iter().enumerate() {
266 let (tx, rx) = mpsc::sync_channel::<ReaderRequest>(READER_WORKER_CHANNEL_CAPACITY);
267 let live = Arc::clone(&live_workers);
268 let handle = thread::Builder::new()
269 .name(format!("fathomdb-reader-{idx}"))
270 .spawn(move || reader_worker_loop(connection, rx, live))
271 .expect("spawn reader worker");
272 senders.push(tx);
273 handles.push(handle);
274 }
275 Self {
276 senders,
277 handles: Mutex::new(Some(handles)),
278 next: AtomicUsize::new(0),
279 shutdown: AtomicBool::new(false),
280 live_workers,
281 }
282 }
283
284 fn worker_count(&self) -> usize {
285 self.senders.len()
286 }
287
288 fn live_count(&self) -> usize {
289 self.live_workers.load(Ordering::SeqCst)
290 }
291
292 #[cfg(debug_assertions)]
297 fn lookaside_used_per_worker(&self) -> Vec<i32> {
298 let mut results = Vec::with_capacity(self.senders.len());
299 for sender in &self.senders {
300 let (tx, rx) = mpsc::sync_channel::<i32>(1);
301 if sender.send(ReaderRequest::LookasideStatus { respond: tx }).is_ok() {
302 results.push(rx.recv().unwrap_or(-1));
303 } else {
304 results.push(-1);
305 }
306 }
307 results
308 }
309
310 #[cfg(debug_assertions)]
316 fn cache_status_per_worker(&self, snapshot_label: &str) -> Vec<CacheStatusReply> {
317 let mut results = Vec::with_capacity(self.senders.len());
318 for (idx, sender) in self.senders.iter().enumerate() {
319 let (tx, rx) = mpsc::sync_channel::<(String, i32, i32, i32)>(1);
320 let request = ReaderRequest::CacheStatus {
321 snapshot_label: snapshot_label.to_string(),
322 respond: tx,
323 };
324 if sender.send(request).is_ok() {
325 if let Ok((label, hit, miss, used)) = rx.recv() {
326 results.push(CacheStatusReply {
327 worker_idx: idx,
328 snapshot_label: label,
329 cache_hit: hit,
330 cache_miss: miss,
331 cache_used_bytes: used,
332 });
333 continue;
334 }
335 }
336 results.push(CacheStatusReply {
337 worker_idx: idx,
338 snapshot_label: snapshot_label.to_string(),
339 cache_hit: -1,
340 cache_miss: -1,
341 cache_used_bytes: -1,
342 });
343 }
344 results
345 }
346
347 fn dispatch(&self, request: ReaderRequest) -> Result<(), ReaderRequest> {
351 if self.shutdown.load(Ordering::Relaxed) {
352 return Err(request);
353 }
354 let n = self.senders.len();
355 if n == 0 {
356 return Err(request);
357 }
358 let idx = self.next.fetch_add(1, Ordering::Relaxed) % n;
359 self.senders[idx].send(request).map_err(|err| err.0)
360 }
361
362 fn shutdown(&self) {
366 if self.shutdown.swap(true, Ordering::SeqCst) {
367 return;
368 }
369 for sender in &self.senders {
370 let _ = sender.send(ReaderRequest::Shutdown);
371 }
372 if let Ok(mut slot) = self.handles.lock() {
373 if let Some(handles) = slot.take() {
374 for handle in handles {
375 let _ = handle.join();
376 }
377 }
378 }
379 }
380}
381
382impl Drop for ReaderWorkerPool {
383 fn drop(&mut self) {
384 self.shutdown();
385 }
386}
387
388fn reader_worker_loop(
389 mut connection: Connection,
390 rx: Receiver<ReaderRequest>,
391 live_workers: Arc<AtomicUsize>,
392) {
393 live_workers.fetch_add(1, Ordering::SeqCst);
394 struct LiveGuard(Arc<AtomicUsize>);
396 impl Drop for LiveGuard {
397 fn drop(&mut self) {
398 self.0.fetch_sub(1, Ordering::SeqCst);
399 }
400 }
401 let _guard = LiveGuard(live_workers);
402
403 while let Ok(request) = rx.recv() {
404 match request {
405 ReaderRequest::Shutdown => break,
406 ReaderRequest::Search { compiled, query_vector, respond } => {
407 let result = read_search_in_tx(&mut connection, &compiled, query_vector.as_deref());
408 let _ = respond.send(result);
411 }
412 #[cfg(debug_assertions)]
413 ReaderRequest::LookasideStatus { respond } => {
414 let _ = respond.send(read_lookaside_used_hiwtr(&connection));
415 }
416 #[cfg(debug_assertions)]
417 ReaderRequest::CacheStatus { snapshot_label, respond } => {
418 let (hit, miss, used) = read_cache_status(&connection);
419 let _ = respond.send((snapshot_label, hit, miss, used));
420 }
421 }
422 }
423
424 uninstall_profile_callback(&connection);
429 drop(connection);
430}
431
432impl ProjectionRuntime {
433 fn new(
434 path: PathBuf,
435 embedder: Option<Arc<dyn Embedder>>,
436 embedder_identity: EmbedderIdentity,
437 ) -> Self {
438 let shared = Arc::new(ProjectionRuntimeShared {
439 path,
440 embedder,
441 embedder_identity,
442 state: Mutex::new(ProjectionRuntimeState::default()),
443 state_cvar: Condvar::new(),
444 queue: Mutex::new(VecDeque::new()),
445 queue_cvar: Condvar::new(),
446 retry_delays_ms: Mutex::new(DEFAULT_PROJECTION_RETRY_DELAYS_MS.to_vec()),
447 });
448
449 let dispatcher_shared = Arc::clone(&shared);
450 let dispatcher = thread::spawn(move || projection_dispatcher_loop(dispatcher_shared));
451
452 let mut workers = Vec::with_capacity(PROJECTION_WORKERS);
453 for _ in 0..PROJECTION_WORKERS {
454 let worker_shared = Arc::clone(&shared);
455 workers.push(thread::spawn(move || projection_worker_loop(worker_shared)));
456 }
457
458 Self { shared, dispatcher: Mutex::new(Some(dispatcher)), workers: Mutex::new(workers) }
459 }
460
461 fn notify_new_work(&self) {
462 if let Ok(mut state) = self.shared.state.lock() {
463 state.pending_scan = true;
464 self.shared.state_cvar.notify_all();
465 }
466 }
467
468 fn set_frozen(&self, frozen: bool) {
469 if let Ok(mut state) = self.shared.state.lock() {
470 state.frozen = frozen;
471 if !frozen {
472 state.pending_scan = true;
473 }
474 self.shared.state_cvar.notify_all();
475 }
476 }
477
478 fn wait_for_idle(&self, timeout_ms: u64) -> bool {
479 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
480 let mut state = match self.shared.state.lock() {
481 Ok(state) => state,
482 Err(_) => return false,
483 };
484 loop {
485 if state.active_jobs == 0 && state.queued_jobs == 0 {
486 drop(state);
487 if !database_has_pending_projection_work(&self.shared.path).unwrap_or(true) {
488 return true;
489 }
490 state = match self.shared.state.lock() {
491 Ok(state) => state,
492 Err(_) => return false,
493 };
494 }
495 let now = Instant::now();
496 if now >= deadline {
497 return false;
498 }
499 let wait = deadline.saturating_duration_since(now);
500 let Ok((next_state, _)) = self.shared.state_cvar.wait_timeout(state, wait) else {
501 return false;
502 };
503 state = next_state;
504 }
505 }
506
507 fn set_retry_delays_for_test(&self, delays_ms: &[u64]) {
508 if let Ok(mut delays) = self.shared.retry_delays_ms.lock() {
509 *delays = delays_ms.to_vec();
510 }
511 }
512
513 fn stop(&self) {
514 if let Ok(mut state) = self.shared.state.lock() {
515 if state.stopping {
516 return;
517 }
518 state.stopping = true;
519 state.pending_scan = false;
520 self.shared.state_cvar.notify_all();
521 }
522 if let Ok(mut queue) = self.shared.queue.lock() {
523 queue.clear();
524 self.shared.queue_cvar.notify_all();
525 }
526
527 if let Ok(mut dispatcher) = self.dispatcher.lock() {
528 if let Some(handle) = dispatcher.take() {
529 let _ = handle.join();
530 }
531 }
532 if let Ok(mut workers) = self.workers.lock() {
533 for handle in workers.drain(..) {
534 let _ = handle.join();
535 }
536 }
537 }
538}
539
540#[derive(Clone, Debug, Eq, PartialEq)]
541pub struct OpenReport {
542 pub schema_version_before: u32,
543 pub schema_version_after: u32,
544 pub migration_steps: Vec<MigrationStepReport>,
545 pub embedder_warmup_ms: u64,
546 pub query_backend: &'static str,
547 pub default_embedder: EmbedderIdentity,
548}
549
550#[derive(Debug)]
551pub struct OpenedEngine {
552 pub engine: Engine,
553 pub report: OpenReport,
554}
555
556#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct WriteReceipt {
558 pub cursor: u64,
559}
560
561#[derive(Clone, Debug, Eq, PartialEq)]
567pub struct SoftFallback {
568 pub branch: SoftFallbackBranch,
569}
570
571#[derive(Clone, Copy, Debug, Eq, PartialEq)]
577pub enum SoftFallbackBranch {
578 Vector,
579 Text,
580}
581
582#[derive(Clone, Debug, Eq, PartialEq)]
583pub struct SearchResult {
584 pub projection_cursor: u64,
585 pub soft_fallback: Option<SoftFallback>,
586 pub results: Vec<String>,
587}
588
589#[non_exhaustive]
595#[derive(Clone, Debug, Eq, PartialEq)]
596pub enum PreparedWrite {
597 Node {
598 kind: String,
599 body: String,
600 source_id: Option<String>,
605 },
606 Edge {
607 kind: String,
608 from: String,
609 to: String,
610 source_id: Option<String>,
612 },
613 OpStore {
614 collection: String,
615 record_key: String,
616 schema_id: Option<String>,
617 body: String,
618 },
619 AdminSchema {
620 name: String,
621 kind: String,
622 schema_json: String,
623 retention_json: String,
624 },
625}
626
627#[derive(Clone, Debug, Default, Eq, PartialEq)]
633pub struct CounterSnapshot {
634 pub queries: u64,
635 pub writes: u64,
636 pub write_rows: u64,
637 pub errors_by_code: BTreeMap<String, u64>,
638 pub admin_ops: u64,
639 pub cache_hit: u64,
640 pub cache_miss: u64,
641}
642
643pub use lifecycle::Subscription;
644
645#[derive(Clone, Debug, Eq, PartialEq)]
650pub struct CorruptionDetail {
651 pub kind: CorruptionKind,
652 pub stage: OpenStage,
653 pub locator: CorruptionLocator,
654 pub recovery_hint: RecoveryHint,
655}
656
657#[derive(Clone, Copy, Debug, Eq, PartialEq)]
663pub enum CorruptionKind {
664 WalReplayFailure,
665 HeaderMalformed,
666 SchemaInconsistent,
667 EmbedderIdentityDrift,
668}
669
670#[derive(Clone, Copy, Debug, Eq, PartialEq)]
676pub enum OpenStage {
677 WalReplay,
678 HeaderProbe,
679 SchemaProbe,
680 EmbedderIdentity,
681}
682
683#[derive(Clone, Copy, Debug, Eq, PartialEq)]
689pub enum CorruptionLocator {
690 FileOffset { offset: u64 },
691 PageId { page: u32 },
692 TableRow { table: &'static str, rowid: i64 },
693 Vec0ShadowRow { partition: &'static str, rowid: i64 },
694 MigrationStep { from: u32, to: u32 },
695 OpaqueSqliteError { sqlite_extended_code: i32 },
696}
697
698#[derive(Clone, Copy, Debug, Eq, PartialEq)]
704pub struct RecoveryHint {
705 pub code: &'static str,
706 pub doc_anchor: &'static str,
707}
708
709#[derive(Clone, Debug, Eq, PartialEq)]
710pub enum EngineOpenError {
711 DatabaseLocked { holder_pid: Option<u32> },
712 Corruption(CorruptionDetail),
713 IncompatibleSchemaVersion { seen: u32, supported: u32 },
714 MigrationError { schema_version_before: u32, schema_version_current: u32, step_id: u32 },
715 EmbedderIdentityMismatch { stored: EmbedderIdentity, supplied: EmbedderIdentity },
716 EmbedderDimensionMismatch { stored: u32, supplied: u32 },
717 Io { message: String },
718}
719
720impl Display for EngineOpenError {
721 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
722 match self {
723 Self::DatabaseLocked { holder_pid } => match holder_pid {
724 Some(pid) => write!(f, "database is locked by process {pid}"),
725 None => write!(f, "database is locked by another engine instance"),
726 },
727 Self::Corruption(detail) => {
728 write!(
729 f,
730 "engine corruption at {:?} stage: {}",
731 detail.stage, detail.recovery_hint.code
732 )
733 }
734 Self::IncompatibleSchemaVersion { seen, supported } => write!(
735 f,
736 "database schema version {seen} is incompatible with supported version {supported}"
737 ),
738 Self::MigrationError {
739 schema_version_before,
740 schema_version_current,
741 step_id,
742 } => write!(
743 f,
744 "schema migration failed at step {step_id}; schema version remained between {schema_version_before} and {schema_version_current}"
745 ),
746 Self::EmbedderIdentityMismatch { stored, supplied } => write!(
747 f,
748 "embedder identity mismatch: stored {}@{}, supplied {}@{}",
749 stored.name, stored.revision, supplied.name, supplied.revision,
750 ),
751 Self::EmbedderDimensionMismatch { stored, supplied } => write!(
752 f,
753 "embedder vector dimension mismatch: stored {stored}, supplied {supplied}",
754 ),
755 Self::Io { message } => write!(f, "database I/O error: {message}"),
756 }
757 }
758}
759
760impl Error for EngineOpenError {}
761
762#[derive(Clone, Debug, Eq, PartialEq)]
763pub enum EngineError {
764 Storage,
765 Projection,
766 Vector,
767 Embedder,
768 EmbedderNotConfigured,
769 KindNotVectorIndexed,
770 EmbedderDimensionMismatch { expected: u32, actual: u32 },
771 Scheduler,
772 OpStore,
773 WriteValidation,
774 SchemaValidation,
775 Overloaded,
776 Closing,
777}
778
779impl Display for EngineError {
780 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
781 match self {
782 Self::Storage => write!(f, "storage error"),
783 Self::Projection => write!(f, "projection error"),
784 Self::Vector => write!(f, "vector error"),
785 Self::Embedder => write!(f, "embedder error"),
786 Self::EmbedderNotConfigured => write!(f, "embedder is not configured"),
787 Self::KindNotVectorIndexed => write!(f, "kind is not configured for vector indexing"),
788 Self::EmbedderDimensionMismatch { expected, actual } => {
789 write!(f, "embedder dimension mismatch: expected {expected}, actual {actual}")
790 }
791 Self::Scheduler => write!(f, "scheduler error"),
792 Self::OpStore => write!(f, "op-store error"),
793 Self::WriteValidation => write!(f, "write validation error"),
794 Self::SchemaValidation => write!(f, "schema validation error"),
795 Self::Overloaded => write!(f, "engine overloaded"),
796 Self::Closing => write!(f, "engine is closing"),
797 }
798 }
799}
800
801impl EngineError {
802 fn stable_code(&self) -> &'static str {
807 match self {
808 Self::Storage => "StorageError",
809 Self::Projection => "ProjectionError",
810 Self::Vector => "VectorError",
811 Self::Embedder => "EmbedderError",
812 Self::EmbedderNotConfigured => "EmbedderNotConfiguredError",
813 Self::KindNotVectorIndexed => "KindNotVectorIndexedError",
814 Self::EmbedderDimensionMismatch { .. } => "EmbedderDimensionMismatchError",
815 Self::Scheduler => "SchedulerError",
816 Self::OpStore => "OpStoreError",
817 Self::WriteValidation => "WriteValidationError",
818 Self::SchemaValidation => "SchemaValidationError",
819 Self::Overloaded => "OverloadedError",
820 Self::Closing => "ClosingError",
821 }
822 }
823}
824
825impl Error for EngineError {}
826
827#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
832pub struct CheckIntegrityOpts {
833 pub quick: bool,
834 pub full: bool,
835 pub round_trip: bool,
836}
837
838#[derive(Clone, Debug, Eq, PartialEq)]
842pub enum Section {
843 Clean,
844 Findings(Vec<Finding>),
845}
846
847#[derive(Clone, Debug, Eq, PartialEq)]
851pub struct Finding {
852 pub code: &'static str,
853 pub stage: &'static str,
854 pub locator: CorruptionLocator,
855 pub doc_anchor: &'static str,
856 pub detail: String,
857}
858
859#[derive(Clone, Debug, Eq, PartialEq)]
862pub struct IntegrityReport {
863 pub physical: Section,
864 pub logical: Section,
865 pub semantic: Section,
866}
867
868#[derive(Clone, Debug, Eq, PartialEq)]
873pub struct SafeExportArtifact {
874 pub export_path: PathBuf,
875 pub manifest_path: PathBuf,
876 pub manifest_sha256: String,
877}
878
879#[derive(Clone, Debug, Eq, PartialEq)]
883pub struct TraceReport {
884 pub source_ref: String,
885 pub events: Vec<TraceEvent>,
886}
887
888#[derive(Clone, Debug, Eq, PartialEq)]
891pub struct TraceEvent {
892 pub write_cursor: u64,
893 pub kind: String,
894 pub table: &'static str,
895}
896
897#[derive(Clone, Copy, Debug, Eq, PartialEq)]
902pub enum RebuildKind {
903 Projections,
904 Vec0,
905}
906
907#[derive(Clone, Debug, Eq, PartialEq)]
914pub struct RebuildReport {
915 pub kind: RebuildKind,
916 pub rows_invalidated: u64,
917 pub rows_rebuilt: u64,
918 pub projection_cursor_after: u64,
919}
920
921#[derive(Clone, Debug, Eq, PartialEq)]
925pub struct ExciseReport {
926 pub source_ref: String,
927 pub nodes_excised: u64,
928 pub edges_excised: u64,
929 pub projections_invalidated: u64,
930}
931
932#[derive(Clone, Copy, Debug, Eq, PartialEq)]
936pub enum VerifyEmbedderStatus {
937 Match,
938 IdentityMismatch,
939 DimensionMismatch,
940 BothMismatch,
941}
942
943#[derive(Clone, Debug, Eq, PartialEq)]
947pub struct VerifyEmbedderReport {
948 pub stored_identity: String,
949 pub stored_dimension: u32,
950 pub supplied_identity: String,
951 pub supplied_dimension: u32,
952 pub status: VerifyEmbedderStatus,
953}
954
955#[derive(Clone, Debug, Eq, PartialEq)]
957pub struct SchemaObject {
958 pub name: String,
959 pub sql: String,
960}
961
962#[derive(Clone, Debug, Eq, PartialEq)]
967pub struct DumpSchemaReport {
968 pub user_version: u32,
969 pub tables: Vec<SchemaObject>,
970 pub indexes: Vec<SchemaObject>,
971}
972
973#[derive(Clone, Debug, Eq, PartialEq)]
975pub struct TableRowCount {
976 pub name: String,
977 pub rows: u64,
978}
979
980#[derive(Clone, Debug, Eq, PartialEq)]
984pub struct DumpRowCountsReport {
985 pub counts: Vec<TableRowCount>,
986}
987
988#[derive(Clone, Debug, Eq, PartialEq)]
992pub struct DumpProfileReport {
993 pub embedder_identity: String,
994 pub embedder_dimension: u32,
995 pub vectorized_kinds: Vec<String>,
996}
997
998#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1002pub enum TruncateWalStatus {
1003 Done,
1004 Busy,
1005}
1006
1007#[derive(Clone, Debug, Eq, PartialEq)]
1011pub struct TruncateWalReport {
1012 pub status: TruncateWalStatus,
1013 pub busy: u32,
1014 pub log_frames: u32,
1015 pub checkpointed_frames: u32,
1016}
1017
1018impl Drop for Engine {
1019 fn drop(&mut self) {
1020 let _ = self.close();
1021 }
1022}
1023
1024impl Engine {
1025 pub fn open(path: impl Into<PathBuf>) -> Result<OpenedEngine, EngineOpenError> {
1026 Self::open_with_embedder_and_subscriber(
1027 path,
1028 default_embedder_identity(),
1029 None,
1030 None,
1031 &mut |_| {},
1032 )
1033 }
1034
1035 pub fn open_with_migration_event_sink(
1036 path: impl Into<PathBuf>,
1037 mut emit_migration_event: impl FnMut(&MigrationStepReport),
1038 ) -> Result<OpenedEngine, EngineOpenError> {
1039 Self::open_with_embedder_and_subscriber(
1040 path,
1041 default_embedder_identity(),
1042 None,
1043 None,
1044 &mut emit_migration_event,
1045 )
1046 }
1047
1048 #[cfg(debug_assertions)]
1049 #[doc(hidden)]
1050 pub fn open_with_migrations_for_test(
1051 path: impl Into<PathBuf>,
1052 migrations: &'static [fathomdb_schema::Migration],
1053 mut emit_migration_event: impl FnMut(&MigrationStepReport),
1054 ) -> Result<OpenedEngine, EngineOpenError> {
1055 Self::open_with_migrations(
1056 path,
1057 migrations,
1058 default_embedder_identity(),
1059 None,
1060 &mut emit_migration_event,
1061 None,
1062 )
1063 }
1064
1065 #[doc(hidden)]
1066 pub fn open_with_subscriber_for_test(
1067 path: impl Into<PathBuf>,
1068 subscriber: Arc<dyn lifecycle::Subscriber>,
1069 ) -> Result<OpenedEngine, EngineOpenError> {
1070 Self::open_with_embedder_and_subscriber(
1071 path,
1072 default_embedder_identity(),
1073 None,
1074 Some(subscriber),
1075 &mut |_| {},
1076 )
1077 }
1078
1079 #[doc(hidden)]
1080 pub fn open_without_embedder_for_test(
1081 path: impl Into<PathBuf>,
1082 ) -> Result<OpenedEngine, EngineOpenError> {
1083 Self::open_with_embedder_and_subscriber(
1084 path,
1085 default_embedder_identity(),
1086 None,
1087 None,
1088 &mut |_| {},
1089 )
1090 }
1091
1092 #[doc(hidden)]
1093 pub fn open_with_embedder_for_test(
1094 path: impl Into<PathBuf>,
1095 embedder: Arc<dyn Embedder>,
1096 ) -> Result<OpenedEngine, EngineOpenError> {
1097 let identity = embedder.identity();
1098 Self::open_with_embedder_and_subscriber(path, identity, Some(embedder), None, &mut |_| {})
1099 }
1100
1101 fn open_with_embedder_and_subscriber(
1102 path: impl Into<PathBuf>,
1103 embedder_identity: EmbedderIdentity,
1104 runtime_embedder: Option<Arc<dyn Embedder>>,
1105 initial_subscriber: Option<Arc<dyn lifecycle::Subscriber>>,
1106 emit_migration_event: &mut impl FnMut(&MigrationStepReport),
1107 ) -> Result<OpenedEngine, EngineOpenError> {
1108 Self::open_with_migrations(
1109 path,
1110 MIGRATIONS,
1111 embedder_identity,
1112 runtime_embedder,
1113 emit_migration_event,
1114 initial_subscriber,
1115 )
1116 }
1117
1118 fn open_with_migrations(
1119 path: impl Into<PathBuf>,
1120 migrations: &'static [fathomdb_schema::Migration],
1121 embedder_identity: EmbedderIdentity,
1122 runtime_embedder: Option<Arc<dyn Embedder>>,
1123 emit_migration_event: &mut impl FnMut(&MigrationStepReport),
1124 initial_subscriber: Option<Arc<dyn lifecycle::Subscriber>>,
1125 ) -> Result<OpenedEngine, EngineOpenError> {
1126 let canonical_path = canonical_database_path(&path.into())?;
1127 let lock = acquire_lock(&canonical_path)?;
1128 let open_result = Self::open_locked(
1129 canonical_path.clone(),
1130 migrations,
1131 &embedder_identity,
1132 emit_migration_event,
1133 );
1134
1135 match open_result {
1136 Ok((connection, readers, report, reader_lookaside_rcs)) => {
1137 let next_cursor = load_next_cursor(&connection);
1138 let subscribers = Arc::new(lifecycle::SubscriberRegistry::new());
1139 let profiling_enabled = Arc::new(AtomicBool::new(false));
1140 let slow_threshold_ms = Arc::new(AtomicU64::new(DEFAULT_SLOW_THRESHOLD_MS));
1141 let mut profile_contexts: Vec<Box<ProfileContext>> = Vec::new();
1142 let projection_runtime = ProjectionRuntime::new(
1143 canonical_path.clone(),
1144 runtime_embedder.clone(),
1145 embedder_identity.clone(),
1146 );
1147
1148 install_profile_callback(
1149 &connection,
1150 &subscribers,
1151 &profiling_enabled,
1152 &slow_threshold_ms,
1153 &mut profile_contexts,
1154 );
1155 for reader in &readers {
1156 install_profile_callback(
1157 reader,
1158 &subscribers,
1159 &profiling_enabled,
1160 &slow_threshold_ms,
1161 &mut profile_contexts,
1162 );
1163 }
1164
1165 let opened = OpenedEngine {
1166 engine: Self {
1167 path: canonical_path.clone(),
1168 next_cursor: AtomicU64::new(next_cursor),
1169 closed: AtomicBool::new(false),
1170 lock: Mutex::new(Some(lock)),
1171 connection: Mutex::new(Some(connection)),
1172 reader_pool: ReaderWorkerPool::new(readers),
1173 counters: lifecycle::Counters::new(),
1174 subscribers,
1175 profiling_enabled,
1176 slow_threshold_ms,
1177 runtime_embedder,
1178 runtime_embedder_identity: embedder_identity,
1179 projection_runtime,
1180 provenance_row_cap: AtomicU64::new(DEFAULT_PROVENANCE_ROW_CAP),
1181 profile_contexts: Mutex::new(profile_contexts),
1182 reader_lookaside_rcs,
1183 #[cfg(debug_assertions)]
1184 force_next_commit_failure: AtomicBool::new(false),
1185 },
1186 report,
1187 };
1188 if let Some(subscriber) = initial_subscriber {
1189 opened.engine.subscribers.attach_persistent(subscriber);
1190 }
1191 if database_has_pending_projection_work(&canonical_path).unwrap_or(false) {
1192 opened.engine.projection_runtime.notify_new_work();
1193 }
1194 Ok(opened)
1195 }
1196 Err(err) => {
1197 if let Some(subscriber) = initial_subscriber {
1198 emit_open_error_event(&subscriber, &err);
1199 }
1200 drop(lock);
1201 Err(err)
1202 }
1203 }
1204 }
1205
1206 fn open_locked(
1207 path: PathBuf,
1208 migrations: &'static [fathomdb_schema::Migration],
1209 embedder_identity: &EmbedderIdentity,
1210 emit_migration_event: &mut impl FnMut(&MigrationStepReport),
1211 ) -> Result<(Connection, Vec<Connection>, OpenReport, Vec<i32>), EngineOpenError> {
1212 register_sqlite_vec_extension();
1213 let connection = Connection::open(&path)
1214 .map_err(|err| map_open_sqlite_error(err, OpenStage::HeaderProbe))?;
1215 probe_database_header(&connection)?;
1223 probe_open_integrity(&connection)?;
1224 probe_wal_sidecar(&path)?;
1225 connection
1226 .pragma_update(None, "journal_mode", "WAL")
1227 .map_err(|err| map_open_sqlite_error(err, OpenStage::WalReplay))?;
1228
1229 reject_legacy_shape(&connection)?;
1230 let migration = migrate_with_event_sink(&connection, migrations, emit_migration_event)
1231 .map_err(map_migration_error)?;
1232 check_embedder_profile(&connection, embedder_identity)?;
1233 ensure_vector_partition(&connection, embedder_identity.dimension).map_err(|_| {
1234 EngineOpenError::Io { message: "could not initialize vector partition".to_string() }
1235 })?;
1236
1237 let warmup_started = Instant::now();
1238 let report = OpenReport {
1239 schema_version_before: migration.schema_version_before,
1240 schema_version_after: migration.schema_version_after,
1241 migration_steps: migration.migration_steps,
1242 embedder_warmup_ms: u64::try_from(warmup_started.elapsed().as_millis())
1243 .unwrap_or(u64::MAX),
1244 query_backend: "fathomdb-query + sqlite-vec",
1245 default_embedder: embedder_identity.clone(),
1246 };
1247
1248 let mut readers = Vec::with_capacity(READER_POOL_SIZE);
1249 let mut lookaside_rcs: Vec<i32> = Vec::with_capacity(READER_POOL_SIZE);
1250 for _ in 0..READER_POOL_SIZE {
1251 let reader = Connection::open(&path)
1252 .map_err(|err| map_open_sqlite_error(err, OpenStage::HeaderProbe))?;
1253 let rc: i32 = configure_reader_lookaside(&reader);
1258 debug_assert_eq!(
1259 rc,
1260 rusqlite::ffi::SQLITE_OK,
1261 "sqlite3_db_config(LOOKASIDE) must return SQLITE_OK on a freshly opened reader",
1262 );
1263 lookaside_rcs.push(rc);
1264 reader
1265 .pragma_update(None, "journal_mode", "WAL")
1266 .map_err(|err| map_open_sqlite_error(err, OpenStage::WalReplay))?;
1267 reader
1268 .pragma_update(None, "query_only", "ON")
1269 .map_err(|err| map_open_sqlite_error(err, OpenStage::SchemaProbe))?;
1270 readers.push(reader);
1271 }
1272
1273 Ok((connection, readers, report, lookaside_rcs))
1274 }
1275
1276 #[must_use]
1277 pub fn path(&self) -> &Path {
1278 &self.path
1279 }
1280
1281 pub fn write(&self, batch: &[PreparedWrite]) -> Result<WriteReceipt, EngineError> {
1282 let category = if batch_is_admin(batch) {
1283 lifecycle::EventCategory::Admin
1284 } else {
1285 lifecycle::EventCategory::Writer
1286 };
1287 self.emit_event(lifecycle::Phase::Started, category, None);
1288 let started = Instant::now();
1289 let outcome = self.write_inner(batch);
1290 self.detect_slow(started, category);
1291 match outcome {
1292 Ok(receipt) => {
1293 let rows = u64::try_from(batch.len()).unwrap_or(u64::MAX);
1294 if batch_is_admin(batch) {
1295 self.counters.record_admin();
1296 } else {
1297 self.counters.record_write(rows);
1298 }
1299 self.emit_event(lifecycle::Phase::Finished, category, None);
1300 Ok(receipt)
1301 }
1302 Err(err) => {
1303 let code = err.stable_code();
1304 self.counters.record_error(code);
1305 self.emit_event(lifecycle::Phase::Failed, category, Some(code));
1308 self.emit_event(
1309 lifecycle::Phase::Failed,
1310 lifecycle::EventCategory::Error,
1311 Some(code),
1312 );
1313 Err(err)
1314 }
1315 }
1316 }
1317
1318 fn write_inner(&self, batch: &[PreparedWrite]) -> Result<WriteReceipt, EngineError> {
1319 self.ensure_open()?;
1320
1321 if batch.is_empty() {
1322 return Err(EngineError::WriteValidation);
1323 }
1324
1325 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1326 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
1327 let plans = validate_batch(connection, batch)?;
1328 let projection_jobs = collect_projection_jobs(connection, batch)?;
1329 #[cfg(debug_assertions)]
1330 if self.force_next_commit_failure.swap(false, Ordering::SeqCst) {
1331 return Err(EngineError::Storage);
1332 }
1333 let increment = u64::try_from(batch.len()).unwrap_or(u64::MAX);
1334 let cursor = self.next_cursor.load(Ordering::SeqCst).saturating_add(increment);
1335 let pending_projection = !projection_jobs.is_empty();
1336
1337 if let Err(err) = commit_batch(
1338 connection,
1339 batch,
1340 &plans,
1341 cursor,
1342 pending_projection,
1343 self.provenance_row_cap.load(Ordering::Relaxed),
1344 ) {
1345 self.emit_sqlite_internal_error(&err);
1346 return Err(EngineError::Storage);
1347 }
1348 self.next_cursor.store(cursor, Ordering::SeqCst);
1349 if pending_projection {
1350 self.projection_runtime.notify_new_work();
1351 }
1352
1353 Ok(WriteReceipt { cursor })
1354 }
1355
1356 pub fn search(&self, query: &str) -> Result<SearchResult, EngineError> {
1357 self.emit_event(lifecycle::Phase::Started, lifecycle::EventCategory::Search, None);
1358 let started = Instant::now();
1359 let outcome = self.search_inner(query);
1360 self.detect_slow(started, lifecycle::EventCategory::Search);
1361 match outcome {
1362 Ok(result) => {
1363 self.counters.record_query();
1364 self.emit_event(lifecycle::Phase::Finished, lifecycle::EventCategory::Search, None);
1365 Ok(result)
1366 }
1367 Err(err) => {
1368 let code = err.stable_code();
1369 self.counters.record_error(code);
1370 self.emit_event(
1371 lifecycle::Phase::Failed,
1372 lifecycle::EventCategory::Search,
1373 Some(code),
1374 );
1375 self.emit_event(
1376 lifecycle::Phase::Failed,
1377 lifecycle::EventCategory::Error,
1378 Some(code),
1379 );
1380 Err(err)
1381 }
1382 }
1383 }
1384
1385 fn detect_slow(&self, started: Instant, category: lifecycle::EventCategory) {
1386 let elapsed = started.elapsed();
1387 let threshold = self.slow_threshold_ms.load(Ordering::Relaxed);
1388 let threshold_duration = std::time::Duration::from_millis(threshold);
1389 if elapsed > threshold_duration {
1390 self.emit_event(lifecycle::Phase::Slow, category, None);
1397 }
1398 }
1399
1400 fn emit_event(
1401 &self,
1402 phase: lifecycle::Phase,
1403 category: lifecycle::EventCategory,
1404 code: Option<&'static str>,
1405 ) {
1406 let event =
1407 lifecycle::Event { phase, source: lifecycle::EventSource::Engine, category, code };
1408 self.subscribers.dispatch(&event);
1409 }
1410
1411 fn emit_sqlite_internal_error(&self, err: &rusqlite::Error) {
1418 if let Some(code) = sqlite_extended_code_name(err) {
1419 let event = lifecycle::Event {
1420 phase: lifecycle::Phase::Failed,
1421 source: lifecycle::EventSource::SqliteInternal,
1422 category: lifecycle::EventCategory::Error,
1423 code: Some(code),
1424 };
1425 self.subscribers.dispatch(&event);
1426 }
1427 }
1428
1429 fn search_inner(&self, query: &str) -> Result<SearchResult, EngineError> {
1430 self.ensure_open()?;
1431 if query.trim().is_empty() {
1432 return Err(EngineError::WriteValidation);
1433 }
1434
1435 let compiled = compile_text_query(query);
1436 let query_vector = self
1445 .runtime_embedder
1446 .as_ref()
1447 .and_then(|embedder| embedder.embed(query).ok())
1448 .and_then(|vector| serde_json::to_string(&vector).ok());
1449 let (response_tx, response_rx) = mpsc::sync_channel::<ReaderResponse>(1);
1450 let request = ReaderRequest::Search { compiled, query_vector, respond: response_tx };
1451 if self.reader_pool.dispatch(request).is_err() {
1452 return Err(EngineError::Closing);
1453 }
1454 let search_result = response_rx.recv().map_err(|_| EngineError::Storage)?;
1455 let (cursor, soft_fallback, results) = match search_result {
1456 Ok(result) => result,
1457 Err(err) => {
1458 self.emit_sqlite_internal_error(&err);
1459 return Err(EngineError::Storage);
1460 }
1461 };
1462
1463 Ok(SearchResult { projection_cursor: cursor, soft_fallback, results })
1464 }
1465
1466 pub fn close(&self) -> Result<(), EngineError> {
1467 self.closed.store(true, Ordering::SeqCst);
1468 self.projection_runtime.stop();
1469 self.reader_pool.shutdown();
1478 if let Ok(mut connection) = self.connection.lock() {
1479 if let Some(conn) = connection.as_ref() {
1480 uninstall_profile_callback(conn);
1481 }
1482 connection.take();
1483 }
1484 if let Ok(mut contexts) = self.profile_contexts.lock() {
1485 contexts.clear();
1486 }
1487 if let Ok(mut lock) = self.lock.lock() {
1488 lock.take();
1489 }
1490 Ok(())
1491 }
1492
1493 pub fn drain(&self, timeout_ms: u64) -> Result<(), EngineError> {
1498 self.ensure_open()?;
1499 if self.projection_runtime.wait_for_idle(timeout_ms) {
1500 Ok(())
1501 } else {
1502 Err(EngineError::Scheduler)
1503 }
1504 }
1505
1506 #[must_use]
1510 pub fn counters(&self) -> CounterSnapshot {
1511 self.counters.snapshot()
1512 }
1513
1514 pub fn set_profiling(&self, enabled: bool) -> Result<(), EngineError> {
1520 self.profiling_enabled.store(enabled, Ordering::Relaxed);
1521 Ok(())
1522 }
1523
1524 pub fn set_slow_threshold_ms(&self, value: u64) -> Result<(), EngineError> {
1530 self.slow_threshold_ms.store(value, Ordering::Relaxed);
1531 Ok(())
1532 }
1533
1534 #[must_use]
1540 pub fn subscribe(&self, subscriber: Arc<dyn lifecycle::Subscriber>) -> Subscription {
1541 self.subscribers.attach(subscriber)
1542 }
1543
1544 #[cfg(debug_assertions)]
1545 #[doc(hidden)]
1546 pub fn reader_worker_count_for_test(&self) -> usize {
1547 self.reader_pool.worker_count()
1548 }
1549
1550 #[cfg(debug_assertions)]
1551 #[doc(hidden)]
1552 pub fn live_reader_worker_count_for_test(&self) -> usize {
1553 self.reader_pool.live_count()
1554 }
1555
1556 #[cfg(debug_assertions)]
1561 #[doc(hidden)]
1562 pub fn reader_lookaside_config_rcs_for_test(&self) -> Vec<i32> {
1563 self.reader_lookaside_rcs.clone()
1564 }
1565
1566 #[cfg(debug_assertions)]
1572 #[doc(hidden)]
1573 pub fn reader_lookaside_used_per_worker_for_test(&self) -> Vec<i32> {
1574 self.reader_pool.lookaside_used_per_worker()
1575 }
1576
1577 #[cfg(debug_assertions)]
1583 #[doc(hidden)]
1584 pub fn cache_status_per_worker_for_test(&self, label: &str) -> Vec<CacheStatusReply> {
1585 self.reader_pool.cache_status_per_worker(label)
1586 }
1587
1588 #[cfg(debug_assertions)]
1589 #[doc(hidden)]
1590 pub fn force_next_commit_failure_for_test(&self) {
1591 self.force_next_commit_failure.store(true, Ordering::SeqCst);
1592 }
1593
1594 #[cfg(debug_assertions)]
1601 #[doc(hidden)]
1602 pub fn execute_for_test(&self, sql: &str) -> Result<(), EngineError> {
1603 self.ensure_open()?;
1604 let started = Instant::now();
1605 {
1606 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1607 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
1608 connection.execute_batch(sql).map_err(|_| EngineError::Storage)?;
1609 }
1610 self.detect_slow(started, lifecycle::EventCategory::Search);
1611 Ok(())
1612 }
1613
1614 #[doc(hidden)]
1625 #[cfg(debug_assertions)]
1626 pub fn run_one_thread_poison_for_test(&self) -> Result<(), EngineError> {
1627 self.ensure_open()?;
1628
1629 self.write(&[PreparedWrite::Node {
1632 kind: "doc".to_string(),
1633 body: "poison-fixture-seed".to_string(),
1634 source_id: None,
1635 }])?;
1636
1637 let poison_outcome: Mutex<Option<EngineError>> = Mutex::new(None);
1638 let poison_thread_id: AtomicU64 = AtomicU64::new(0);
1639
1640 thread::scope(|scope| {
1641 for _ in 0..4 {
1643 scope.spawn(|| {
1644 for _ in 0..4 {
1645 let _ = self.search("poison-fixture-seed");
1646 }
1647 });
1648 }
1649 scope.spawn(|| {
1651 let _ = self.write(&[PreparedWrite::Node {
1652 kind: "doc".to_string(),
1653 body: "writer-progress".to_string(),
1654 source_id: None,
1655 }]);
1656 });
1657 scope.spawn(|| {
1660 poison_thread_id.store(1, Ordering::SeqCst);
1663 if let Err(err) = self.write(&[]) {
1664 *poison_outcome.lock().expect("poison_outcome lock") = Some(err);
1665 }
1666 });
1667 });
1668
1669 let err = poison_outcome
1670 .into_inner()
1671 .expect("poison_outcome lock")
1672 .expect("poison thread must produce a deterministic error");
1673
1674 let projection_state = match self.projection_status_for_test("doc") {
1675 Ok(lifecycle::ProjectionStatus::Pending) => "Pending",
1676 Ok(lifecycle::ProjectionStatus::Failed) => "Failed",
1677 Ok(lifecycle::ProjectionStatus::UpToDate) => "UpToDate",
1678 Err(_) => "UpToDate",
1683 };
1684
1685 let context = lifecycle::StressFailureContext {
1686 thread_group_id: poison_thread_id.load(Ordering::SeqCst),
1687 op_kind: "write".to_string(),
1688 last_error_chain: vec![err.stable_code().to_string(), err.to_string()],
1689 projection_state: projection_state.to_string(),
1690 };
1691 self.subscribers.dispatch_stress_failure(&context);
1692 Ok(())
1693 }
1694
1695 #[doc(hidden)]
1696 pub fn set_projection_scheduler_frozen_for_test(&self, frozen: bool) {
1697 self.projection_runtime.set_frozen(frozen);
1698 }
1699
1700 #[doc(hidden)]
1701 pub fn set_projection_retry_delays_for_test(&self, delays_ms: &[u64]) {
1702 self.projection_runtime.set_retry_delays_for_test(delays_ms);
1703 }
1704
1705 #[doc(hidden)]
1706 pub fn projection_status_for_test(
1707 &self,
1708 kind: &str,
1709 ) -> Result<lifecycle::ProjectionStatus, EngineError> {
1710 self.ensure_open()?;
1711 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1712 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1713 projection_status(connection, kind)
1714 }
1715
1716 #[doc(hidden)]
1717 pub fn has_vector_for_cursor_for_test(&self, cursor: u64) -> Result<bool, EngineError> {
1718 self.ensure_open()?;
1719 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1720 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1721 terminal_state_for_cursor(connection, cursor)
1722 .map(|state| matches!(state.as_deref(), Some("up_to_date")))
1723 .map_err(|_| EngineError::Storage)
1724 }
1725
1726 #[doc(hidden)]
1727 pub fn projection_failure_count_for_test(&self, cursor: u64) -> Result<u64, EngineError> {
1728 self.ensure_open()?;
1729 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1730 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1731 connection
1732 .query_row(
1733 "SELECT COUNT(*) FROM operational_mutations
1734 WHERE collection_name = 'projection_failures'
1735 AND record_key = ?1",
1736 [cursor.to_string()],
1737 |row| row.get::<_, u64>(0),
1738 )
1739 .map_err(|_| EngineError::Storage)
1740 }
1741
1742 #[doc(hidden)]
1743 pub fn set_provenance_row_cap_for_test(&self, cap: Option<u64>) {
1744 self.provenance_row_cap.store(cap.unwrap_or(0), Ordering::Relaxed);
1745 }
1746
1747 #[doc(hidden)]
1748 pub fn provenance_row_count_for_test(&self) -> Result<u64, EngineError> {
1749 self.ensure_open()?;
1750 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1751 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1752 connection
1753 .query_row("SELECT COUNT(*) FROM operational_mutations", [], |row| row.get::<_, u64>(0))
1754 .map_err(|_| EngineError::Storage)
1755 }
1756
1757 #[doc(hidden)]
1758 pub fn oldest_provenance_record_key_for_test(
1759 &self,
1760 collection: &str,
1761 ) -> Result<Option<String>, EngineError> {
1762 self.ensure_open()?;
1763 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1764 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1765 connection
1766 .query_row(
1767 "SELECT record_key FROM operational_mutations
1768 WHERE collection_name = ?1
1769 ORDER BY id
1770 LIMIT 1",
1771 [collection],
1772 |row| row.get::<_, String>(0),
1773 )
1774 .map(Some)
1775 .or_else(|err| match err {
1776 rusqlite::Error::QueryReturnedNoRows => Ok(None),
1777 _ => Err(EngineError::Storage),
1778 })
1779 }
1780
1781 #[doc(hidden)]
1782 pub fn configure_vector_kind_for_test(&self, kind: &str) -> Result<(), EngineError> {
1783 self.ensure_open()?;
1784 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1785 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
1786 connection
1787 .execute(
1788 "INSERT OR REPLACE INTO _fathomdb_vector_kinds(kind, profile, created_at)
1789 VALUES(?1, ?2, 0)",
1790 params![kind, DEFAULT_VECTOR_PROFILE],
1791 )
1792 .map_err(|_| EngineError::Storage)?;
1793 Ok(())
1794 }
1795
1796 #[doc(hidden)]
1797 pub fn write_vector_for_test(
1798 &self,
1799 kind: &str,
1800 text: &str,
1801 ) -> Result<WriteReceipt, EngineError> {
1802 self.ensure_open()?;
1803 let embedder =
1804 self.runtime_embedder.as_ref().cloned().ok_or(EngineError::EmbedderNotConfigured)?;
1805
1806 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1807 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
1808 if !kind_is_vector_indexed(connection, kind)? {
1809 return Err(EngineError::KindNotVectorIndexed);
1810 }
1811
1812 let expected = default_profile_dimension(connection)?;
1813 ensure_vector_partition(connection, expected).map_err(|_| EngineError::Storage)?;
1814 let vector = embedder.embed(text).map_err(map_runtime_embedder_error)?;
1815 let actual = u32::try_from(vector.len()).unwrap_or(u32::MAX);
1816 if actual != expected {
1817 return Err(EngineError::EmbedderDimensionMismatch { expected, actual });
1818 }
1819
1820 let cursor = self.next_cursor.load(Ordering::SeqCst).saturating_add(1);
1821 let blob = encode_vector_blob(&vector);
1822 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
1823 tx.execute(
1824 "INSERT INTO _fathomdb_vector_rows(rowid, kind, write_cursor) VALUES(?1, ?2, ?3)",
1825 params![cursor, kind, cursor],
1826 )
1827 .map_err(|_| EngineError::Storage)?;
1828 tx.execute(
1829 "INSERT INTO vector_default(rowid, embedding) VALUES(?1, ?2)",
1830 params![cursor, blob],
1831 )
1832 .map_err(|_| EngineError::Storage)?;
1833 tx.commit().map_err(|_| EngineError::Storage)?;
1834 self.next_cursor.store(cursor, Ordering::SeqCst);
1835 Ok(WriteReceipt { cursor })
1836 }
1837
1838 #[doc(hidden)]
1839 pub fn vector_row_count_for_test(&self) -> Result<u64, EngineError> {
1840 self.ensure_open()?;
1841 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1842 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1843 connection
1844 .query_row("SELECT COUNT(*) FROM vector_default", [], |row| row.get::<_, u64>(0))
1845 .map_err(|_| EngineError::Storage)
1846 }
1847
1848 #[doc(hidden)]
1849 pub fn read_vector_blob_for_test(&self, rowid: i64) -> Result<Vec<u8>, EngineError> {
1850 self.ensure_open()?;
1851 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1852 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1853 connection
1854 .query_row("SELECT embedding FROM vector_default WHERE rowid = ?1", [rowid], |row| {
1855 row.get::<_, Vec<u8>>(0)
1856 })
1857 .map_err(|_| EngineError::Storage)
1858 }
1859
1860 #[doc(hidden)]
1861 pub fn default_embedder_profile_for_test(&self) -> Result<EmbedderIdentity, EngineError> {
1862 self.ensure_open()?;
1863 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1864 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1865 load_default_profile(connection).map_err(|_| EngineError::Storage)
1866 }
1867
1868 pub fn check_integrity(
1872 &self,
1873 opts: CheckIntegrityOpts,
1874 ) -> Result<IntegrityReport, EngineError> {
1875 self.ensure_open()?;
1876 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1877 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1878 Ok(IntegrityReport {
1879 physical: physical_section(connection, opts.full),
1880 logical: logical_section(connection),
1881 semantic: semantic_section(connection),
1882 })
1883 }
1884
1885 pub fn safe_export(
1890 &self,
1891 out: &Path,
1892 manifest: &Path,
1893 ) -> Result<SafeExportArtifact, EngineError> {
1894 self.ensure_open()?;
1895 {
1896 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1897 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1898 let target = out.to_string_lossy().to_string();
1899 connection
1900 .execute("VACUUM INTO ?1", params![target])
1901 .map_err(|_| EngineError::Storage)?;
1902 }
1903 let bytes = std::fs::read(out).map_err(|_| EngineError::Storage)?;
1904 let digest = sha2::Sha256::digest(&bytes);
1905 let sha256_hex = hex_encode(digest.as_slice());
1906 let export_abs = out.canonicalize().unwrap_or_else(|_| out.to_path_buf());
1907 let manifest_json = serde_json::json!({
1908 "export_path": export_abs.to_string_lossy(),
1909 "sha256": sha256_hex,
1910 "byte_count": bytes.len() as u64,
1911 });
1912 let manifest_bytes =
1913 serde_json::to_vec_pretty(&manifest_json).map_err(|_| EngineError::Storage)?;
1914 std::fs::write(manifest, &manifest_bytes).map_err(|_| EngineError::Storage)?;
1915 Ok(SafeExportArtifact {
1916 export_path: out.to_path_buf(),
1917 manifest_path: manifest.to_path_buf(),
1918 manifest_sha256: sha256_hex,
1919 })
1920 }
1921
1922 pub fn rebuild_projections(&self) -> Result<RebuildReport, EngineError> {
1929 self.ensure_open()?;
1930 self.run_rebuild(true, RebuildKind::Projections)
1931 }
1932
1933 pub fn rebuild_vec0(&self) -> Result<RebuildReport, EngineError> {
1937 self.ensure_open()?;
1938 self.run_rebuild(false, RebuildKind::Vec0)
1939 }
1940
1941 pub fn trace_source_ref(&self, source_id: &str) -> Result<TraceReport, EngineError> {
1946 self.ensure_open()?;
1947 if source_id.is_empty() {
1948 return Err(EngineError::WriteValidation);
1949 }
1950 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
1951 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
1952
1953 let mut events: Vec<TraceEvent> = Vec::new();
1954 let mut nodes = connection
1955 .prepare(
1956 "SELECT write_cursor, kind FROM canonical_nodes WHERE source_id = ?1
1957 ORDER BY write_cursor",
1958 )
1959 .map_err(|_| EngineError::Storage)?;
1960 let node_rows = nodes
1961 .query_map([source_id], |row| {
1962 Ok(TraceEvent {
1963 write_cursor: row.get::<_, i64>(0)? as u64,
1964 kind: row.get::<_, String>(1)?,
1965 table: "canonical_nodes",
1966 })
1967 })
1968 .map_err(|_| EngineError::Storage)?;
1969 for row in node_rows {
1970 events.push(row.map_err(|_| EngineError::Storage)?);
1971 }
1972
1973 let mut edges = connection
1974 .prepare(
1975 "SELECT write_cursor, kind FROM canonical_edges WHERE source_id = ?1
1976 ORDER BY write_cursor",
1977 )
1978 .map_err(|_| EngineError::Storage)?;
1979 let edge_rows = edges
1980 .query_map([source_id], |row| {
1981 Ok(TraceEvent {
1982 write_cursor: row.get::<_, i64>(0)? as u64,
1983 kind: row.get::<_, String>(1)?,
1984 table: "canonical_edges",
1985 })
1986 })
1987 .map_err(|_| EngineError::Storage)?;
1988 for row in edge_rows {
1989 events.push(row.map_err(|_| EngineError::Storage)?);
1990 }
1991
1992 events.sort_by_key(|e| e.write_cursor);
1993 Ok(TraceReport { source_ref: source_id.to_string(), events })
1994 }
1995
1996 pub fn excise_source(&self, source_id: &str) -> Result<ExciseReport, EngineError> {
2006 self.ensure_open()?;
2007 if source_id.is_empty() {
2008 return Err(EngineError::WriteValidation);
2009 }
2010
2011 self.projection_runtime.set_frozen(true);
2018 let drain_result = self.drain(REBUILD_DRAIN_TIMEOUT_MS);
2019 let outcome = drain_result.and_then(|()| self.excise_source_inner(source_id));
2020 self.projection_runtime.set_frozen(false);
2021 outcome
2022 }
2023
2024 pub fn verify_embedder(
2028 &self,
2029 supplied_identity: &str,
2030 supplied_dimension: u32,
2031 ) -> Result<VerifyEmbedderReport, EngineError> {
2032 self.ensure_open()?;
2033 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2034 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2035 let stored = load_default_profile(connection).map_err(|_| EngineError::Storage)?;
2036 let stored_identity = format!("{}:{}", stored.name, stored.revision);
2037 let identity_match = stored_identity == supplied_identity;
2038 let dimension_match = stored.dimension == supplied_dimension;
2039 let status = match (identity_match, dimension_match) {
2040 (true, true) => VerifyEmbedderStatus::Match,
2041 (false, true) => VerifyEmbedderStatus::IdentityMismatch,
2042 (true, false) => VerifyEmbedderStatus::DimensionMismatch,
2043 (false, false) => VerifyEmbedderStatus::BothMismatch,
2044 };
2045 Ok(VerifyEmbedderReport {
2046 stored_identity,
2047 stored_dimension: stored.dimension,
2048 supplied_identity: supplied_identity.to_string(),
2049 supplied_dimension,
2050 status,
2051 })
2052 }
2053
2054 pub fn dump_schema(&self) -> Result<DumpSchemaReport, EngineError> {
2059 self.ensure_open()?;
2060 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2061 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2062 let user_version: u32 = connection
2063 .query_row("PRAGMA user_version", [], |row| row.get(0))
2064 .map_err(|_| EngineError::Storage)?;
2065 let tables = read_schema_objects(connection, "table")?;
2066 let indexes = read_schema_objects(connection, "index")?;
2067 Ok(DumpSchemaReport { user_version, tables: order_canonical_first(tables), indexes })
2068 }
2069
2070 pub fn dump_row_counts(&self) -> Result<DumpRowCountsReport, EngineError> {
2073 self.ensure_open()?;
2074 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2075 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2076 let mut counts = Vec::with_capacity(CANONICAL_TABLES.len());
2077 for name in CANONICAL_TABLES {
2078 let rows: u64 = connection
2079 .query_row(&format!("SELECT COUNT(*) FROM {name}"), [], |row| row.get(0))
2080 .map_err(|_| EngineError::Storage)?;
2081 counts.push(TableRowCount { name: (*name).to_string(), rows });
2082 }
2083 Ok(DumpRowCountsReport { counts })
2084 }
2085
2086 pub fn dump_profile(&self) -> Result<DumpProfileReport, EngineError> {
2090 self.ensure_open()?;
2091 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2092 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2093 let stored = load_default_profile(connection).map_err(|_| EngineError::Storage)?;
2094 let mut stmt = connection
2095 .prepare("SELECT kind FROM _fathomdb_vector_kinds ORDER BY kind")
2096 .map_err(|_| EngineError::Storage)?;
2097 let rows =
2098 stmt.query_map([], |row| row.get::<_, String>(0)).map_err(|_| EngineError::Storage)?;
2099 let mut vectorized_kinds = Vec::new();
2100 for row in rows {
2101 vectorized_kinds.push(row.map_err(|_| EngineError::Storage)?);
2102 }
2103 Ok(DumpProfileReport {
2104 embedder_identity: format!("{}:{}", stored.name, stored.revision),
2105 embedder_dimension: stored.dimension,
2106 vectorized_kinds,
2107 })
2108 }
2109
2110 pub fn truncate_wal(&self) -> Result<TruncateWalReport, EngineError> {
2116 self.ensure_open()?;
2117 let connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2118 let connection = connection.as_ref().ok_or(EngineError::Closing)?;
2119 let (busy, log_frames, checkpointed_frames): (i64, i64, i64) = connection
2120 .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
2121 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
2122 })
2123 .map_err(|_| EngineError::Storage)?;
2124 let status = if busy == 0 { TruncateWalStatus::Done } else { TruncateWalStatus::Busy };
2125 Ok(TruncateWalReport {
2126 status,
2127 busy: busy.max(0) as u32,
2128 log_frames: log_frames.max(0) as u32,
2129 checkpointed_frames: checkpointed_frames.max(0) as u32,
2130 })
2131 }
2132
2133 fn excise_source_inner(&self, source_id: &str) -> Result<ExciseReport, EngineError> {
2134 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2135 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2136 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
2137
2138 let node_cursors: Vec<i64> = {
2141 let mut stmt = tx
2142 .prepare("SELECT write_cursor FROM canonical_nodes WHERE source_id = ?1")
2143 .map_err(|_| EngineError::Storage)?;
2144 let rows = stmt
2145 .query_map([source_id], |row| row.get::<_, i64>(0))
2146 .map_err(|_| EngineError::Storage)?;
2147 rows.collect::<rusqlite::Result<Vec<_>>>().map_err(|_| EngineError::Storage)?
2148 };
2149 let edge_cursors: Vec<i64> = {
2150 let mut stmt = tx
2151 .prepare("SELECT write_cursor FROM canonical_edges WHERE source_id = ?1")
2152 .map_err(|_| EngineError::Storage)?;
2153 let rows = stmt
2154 .query_map([source_id], |row| row.get::<_, i64>(0))
2155 .map_err(|_| EngineError::Storage)?;
2156 rows.collect::<rusqlite::Result<Vec<_>>>().map_err(|_| EngineError::Storage)?
2157 };
2158
2159 let mut shadow_invalidated: u64 = 0;
2160 for cursor in node_cursors.iter().chain(edge_cursors.iter()) {
2161 shadow_invalidated = shadow_invalidated.saturating_add(
2162 tx.execute("DELETE FROM search_index WHERE write_cursor = ?1", [cursor])
2163 .map_err(|_| EngineError::Storage)? as u64,
2164 );
2165 shadow_invalidated = shadow_invalidated.saturating_add(
2168 tx.execute("DELETE FROM vector_default WHERE rowid = ?1", [cursor])
2169 .map_err(|_| EngineError::Storage)? as u64,
2170 );
2171 shadow_invalidated = shadow_invalidated.saturating_add(
2172 tx.execute("DELETE FROM _fathomdb_vector_rows WHERE write_cursor = ?1", [cursor])
2173 .map_err(|_| EngineError::Storage)? as u64,
2174 );
2175 shadow_invalidated = shadow_invalidated.saturating_add(
2176 tx.execute(
2177 "DELETE FROM _fathomdb_projection_terminal WHERE write_cursor = ?1",
2178 [cursor],
2179 )
2180 .map_err(|_| EngineError::Storage)? as u64,
2181 );
2182 }
2183
2184 let nodes_excised = tx
2185 .execute("DELETE FROM canonical_nodes WHERE source_id = ?1", [source_id])
2186 .map_err(|_| EngineError::Storage)? as u64;
2187 let edges_excised = tx
2188 .execute("DELETE FROM canonical_edges WHERE source_id = ?1", [source_id])
2189 .map_err(|_| EngineError::Storage)? as u64;
2190
2191 let excised_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
2198 let payload = serde_json::json!({
2199 "source_id": source_id,
2200 "excised_at": excised_at,
2201 "nodes_excised": nodes_excised,
2202 "edges_excised": edges_excised,
2203 "projections_invalidated": shadow_invalidated,
2204 })
2205 .to_string();
2206 let audit_cursor = self.next_cursor.load(Ordering::SeqCst).saturating_add(1);
2207 tx.execute(
2208 "INSERT INTO operational_mutations(
2209 collection_name, record_key, op_kind, payload_json, schema_id, write_cursor
2210 ) VALUES('excise_source_audit', ?1, 'append', ?2, NULL, ?3)",
2211 params![source_id, payload, audit_cursor],
2212 )
2213 .map_err(|_| EngineError::Storage)?;
2214
2215 tx.commit().map_err(|_| EngineError::Storage)?;
2216 self.next_cursor.store(audit_cursor, Ordering::SeqCst);
2217 Ok(ExciseReport {
2218 source_ref: source_id.to_string(),
2219 nodes_excised,
2220 edges_excised,
2221 projections_invalidated: shadow_invalidated,
2222 })
2223 }
2224
2225 fn run_rebuild(
2226 &self,
2227 include_fts: bool,
2228 kind: RebuildKind,
2229 ) -> Result<RebuildReport, EngineError> {
2230 self.projection_runtime.set_frozen(true);
2231 let drain_result = self.drain(REBUILD_DRAIN_TIMEOUT_MS);
2238 let result = drain_result.and_then(|()| self.rebuild_shadow_state(include_fts, kind));
2239 self.projection_runtime.set_frozen(false);
2240 result
2241 }
2242
2243 fn rebuild_shadow_state(
2244 &self,
2245 include_fts: bool,
2246 kind: RebuildKind,
2247 ) -> Result<RebuildReport, EngineError> {
2248 let mut connection = self.connection.lock().map_err(|_| EngineError::Storage)?;
2249 let connection = connection.as_mut().ok_or(EngineError::Closing)?;
2250 let tx = connection.transaction().map_err(|_| EngineError::Storage)?;
2251 let mut rows_invalidated: u64 = 0;
2252 if include_fts {
2253 let n = tx.execute("DELETE FROM search_index", []).map_err(|_| EngineError::Storage)?;
2254 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2255 }
2256 let n = tx.execute("DELETE FROM vector_default", []).map_err(|_| EngineError::Storage)?;
2257 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2258 let n = tx
2259 .execute("DELETE FROM _fathomdb_vector_rows", [])
2260 .map_err(|_| EngineError::Storage)?;
2261 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2262 let n = tx
2263 .execute("DELETE FROM _fathomdb_projection_terminal", [])
2264 .map_err(|_| EngineError::Storage)?;
2265 rows_invalidated = rows_invalidated.saturating_add(n as u64);
2266 store_projection_cursor(&tx, 0).map_err(|_| EngineError::Storage)?;
2267 let mut rows_rebuilt: u64 = 0;
2268 if include_fts {
2269 for row in canonical_node_rows(&tx).map_err(|_| EngineError::Storage)? {
2270 tx.execute(
2271 "INSERT INTO search_index(body, kind, write_cursor) VALUES(?1, ?2, ?3)",
2272 params![row.body, row.kind, row.cursor],
2273 )
2274 .map_err(|_| EngineError::Storage)?;
2275 rows_rebuilt = rows_rebuilt.saturating_add(1);
2276 }
2277 }
2278 let projection_cursor_after =
2279 load_projection_cursor(&tx).map_err(|_| EngineError::Storage)?;
2280 tx.commit().map_err(|_| EngineError::Storage)?;
2281 Ok(RebuildReport { kind, rows_invalidated, rows_rebuilt, projection_cursor_after })
2282 }
2283
2284 fn ensure_open(&self) -> Result<(), EngineError> {
2285 if self.closed.load(Ordering::SeqCst) {
2286 return Err(EngineError::Closing);
2287 }
2288
2289 Ok(())
2290 }
2291}
2292
2293fn batch_is_admin(batch: &[PreparedWrite]) -> bool {
2294 !batch.is_empty() && batch.iter().all(|w| matches!(w, PreparedWrite::AdminSchema { .. }))
2295}
2296
2297fn read_search_in_tx(
2299 reader: &mut Connection,
2300 compiled: &fathomdb_query::CompiledQuery,
2301 query_vector: Option<&str>,
2302) -> rusqlite::Result<(u64, Option<SoftFallback>, Vec<String>)> {
2303 let tx = reader.transaction_with_behavior(rusqlite::TransactionBehavior::Deferred)?;
2304 let cursor = load_projection_cursor(&tx)?;
2305 let vector_results = if let Some(query_vector) = query_vector {
2306 let mut rowids = Vec::new();
2307 {
2308 let mut statement = tx.prepare(
2309 "SELECT rowid
2310 FROM vector_default
2311 WHERE embedding MATCH vec_f32(?1)
2312 ORDER BY distance
2313 LIMIT 10",
2314 )?;
2315 let rows = statement.query_map([query_vector], |row| row.get::<_, i64>(0))?;
2316 for row in rows.flatten() {
2317 rowids.push(row);
2318 }
2319 }
2320 let mut results = Vec::new();
2321 let mut statement =
2322 tx.prepare("SELECT body FROM canonical_nodes WHERE write_cursor = ?1 LIMIT 1")?;
2323 for rowid in rowids {
2324 if let Ok(body) = statement.query_row([rowid], |row| row.get::<_, String>(0)) {
2325 results.push(body);
2326 }
2327 }
2328 results
2329 } else {
2330 Vec::new()
2331 };
2332 let vector_rows_visible = !vector_results.is_empty();
2333 let soft_fallback = if query_vector.is_some() && !vector_rows_visible {
2334 tx.query_row(
2335 "SELECT 1
2336 FROM search_index
2337 JOIN _fathomdb_vector_kinds ON _fathomdb_vector_kinds.kind = search_index.kind
2338 LEFT JOIN _fathomdb_projection_terminal
2339 ON _fathomdb_projection_terminal.write_cursor = search_index.write_cursor
2340 WHERE search_index MATCH ?1
2341 AND _fathomdb_projection_terminal.write_cursor IS NULL
2342 LIMIT 1",
2343 [compiled.match_expression.as_str()],
2344 |_row| Ok(SoftFallback { branch: SoftFallbackBranch::Vector }),
2345 )
2346 .ok()
2347 } else {
2348 None
2349 };
2350 let mut seen = BTreeSet::new();
2351 let mut results = Vec::new();
2352 for row in vector_results {
2353 if seen.insert(row.clone()) {
2354 results.push(row);
2355 }
2356 }
2357 {
2358 let mut statement = tx.prepare(
2359 "SELECT body FROM search_index WHERE search_index MATCH ?1 ORDER BY write_cursor",
2360 )?;
2361 let rows = statement
2362 .query_map([compiled.match_expression.as_str()], |row| row.get::<_, String>(0))?;
2363 for row in rows.flatten() {
2364 if seen.insert(row.clone()) {
2365 results.push(row);
2366 }
2367 }
2368 }
2369 tx.commit()?;
2370 Ok((cursor, soft_fallback, results))
2371}
2372
2373fn projection_dispatcher_loop(shared: Arc<ProjectionRuntimeShared>) {
2374 let connection = match open_runtime_connection(&shared.path) {
2375 Ok(connection) => connection,
2376 Err(_) => return,
2377 };
2378 loop {
2379 let in_flight = {
2380 let mut state = match shared.state.lock() {
2381 Ok(state) => state,
2382 Err(_) => return,
2383 };
2384 while !state.stopping
2385 && (!state.pending_scan
2386 || state.frozen
2387 || state.active_jobs + state.queued_jobs >= PROJECTION_INFLIGHT_LIMIT)
2388 {
2389 state = match shared.state_cvar.wait(state) {
2390 Ok(state) => state,
2391 Err(_) => return,
2392 };
2393 }
2394 if state.stopping {
2395 return;
2396 }
2397 state.pending_scan = false;
2398 state.in_flight.clone()
2399 };
2400
2401 match next_pending_projection_job(&connection, &in_flight) {
2402 Ok(Some(job)) => {
2403 if let Ok(mut state) = shared.state.lock() {
2404 state.queued_jobs = state.queued_jobs.saturating_add(1);
2405 state.in_flight.insert(job.cursor);
2406 state.pending_scan = true;
2407 shared.state_cvar.notify_all();
2408 }
2409 if let Ok(mut queue) = shared.queue.lock() {
2410 queue.push_back(job);
2411 shared.queue_cvar.notify_one();
2412 }
2413 }
2414 Ok(None) => {}
2415 Err(_) => {
2416 if let Ok(mut state) = shared.state.lock() {
2417 state.pending_scan = false;
2418 shared.state_cvar.notify_all();
2419 }
2420 }
2421 }
2422 }
2423}
2424
2425fn projection_worker_loop(shared: Arc<ProjectionRuntimeShared>) {
2426 let mut connection = match open_runtime_connection(&shared.path) {
2427 Ok(connection) => connection,
2428 Err(_) => return,
2429 };
2430 if ensure_vector_partition(&connection, shared.embedder_identity.dimension).is_err() {
2431 return;
2432 }
2433 loop {
2434 let jobs = {
2435 let mut queue = match shared.queue.lock() {
2436 Ok(queue) => queue,
2437 Err(_) => return,
2438 };
2439 loop {
2440 let stopping = shared.state.lock().map(|state| state.stopping).unwrap_or(true);
2441 if stopping && queue.is_empty() {
2442 return;
2443 }
2444 if let Some(job) = queue.pop_front() {
2445 let mut jobs = vec![job];
2446 while jobs.len() < PROJECTION_COMMIT_BATCH {
2447 let Some(job) = queue.pop_front() else {
2448 break;
2449 };
2450 jobs.push(job);
2451 }
2452 if let Ok(mut state) = shared.state.lock() {
2453 state.queued_jobs = state.queued_jobs.saturating_sub(jobs.len());
2454 state.active_jobs = state.active_jobs.saturating_add(jobs.len());
2455 shared.state_cvar.notify_all();
2456 }
2457 break jobs;
2458 }
2459 queue = match shared.queue_cvar.wait(queue) {
2460 Ok(queue) => queue,
2461 Err(_) => return,
2462 };
2463 }
2464 };
2465
2466 run_projection_jobs(&shared, &mut connection, &jobs);
2467
2468 if let Ok(mut state) = shared.state.lock() {
2469 state.active_jobs = state.active_jobs.saturating_sub(jobs.len());
2470 for job in &jobs {
2471 state.in_flight.remove(&job.cursor);
2472 }
2473 if !state.stopping {
2474 state.pending_scan = true;
2475 }
2476 shared.state_cvar.notify_all();
2477 }
2478 }
2479}
2480
2481enum ProjectionOutcome {
2482 Success { cursor: u64, kind: String, blob: Vec<u8> },
2483 Failure { cursor: u64, failure_code: &'static str },
2484}
2485
2486fn run_projection_jobs(
2487 shared: &ProjectionRuntimeShared,
2488 connection: &mut Connection,
2489 jobs: &[ProjectionJob],
2490) {
2491 let mut outcomes = Vec::with_capacity(jobs.len());
2492 for job in jobs {
2493 outcomes.push(run_projection_job(shared, job));
2494 }
2495 let _ = commit_projection_outcomes(connection, &outcomes);
2496}
2497
2498fn run_projection_job(shared: &ProjectionRuntimeShared, job: &ProjectionJob) -> ProjectionOutcome {
2499 let delays = shared.retry_delays_ms.lock().map(|delays| delays.clone()).unwrap_or_default();
2500 let mut last_code = "EmbedderError";
2501 for (attempt, delay_ms) in std::iter::once(0_u64).chain(delays.iter().copied()).enumerate() {
2502 if attempt > 0 {
2503 if shared.state.lock().map(|state| state.stopping).unwrap_or(true) {
2504 return ProjectionOutcome::Failure { cursor: job.cursor, failure_code: last_code };
2505 }
2506 thread::sleep(Duration::from_millis(delay_ms));
2507 }
2508 let vector = match shared.embedder.as_ref() {
2509 Some(embedder) => match embedder.embed(&job.body) {
2510 Ok(vector) => vector,
2511 Err(RuntimeEmbedderError::Timeout) => {
2512 last_code = "EmbedderError";
2513 continue;
2514 }
2515 Err(RuntimeEmbedderError::Failed { .. }) => {
2516 last_code = "EmbedderError";
2517 continue;
2518 }
2519 },
2520 None => {
2521 last_code = "EmbedderNotConfiguredError";
2522 continue;
2523 }
2524 };
2525
2526 if u32::try_from(vector.len()).unwrap_or(u32::MAX) != shared.embedder_identity.dimension {
2527 last_code = "EmbedderDimensionMismatchError";
2528 continue;
2529 }
2530
2531 let blob = encode_vector_blob(&vector);
2532 return ProjectionOutcome::Success { cursor: job.cursor, kind: job.kind.clone(), blob };
2533 }
2534
2535 ProjectionOutcome::Failure { cursor: job.cursor, failure_code: last_code }
2536}
2537
2538fn next_pending_projection_job(
2539 connection: &Connection,
2540 in_flight: &BTreeSet<u64>,
2541) -> rusqlite::Result<Option<ProjectionJob>> {
2542 let cursor = load_projection_cursor(connection)?;
2543 let mut statement = connection.prepare_cached(
2544 "SELECT canonical_nodes.write_cursor, canonical_nodes.kind, canonical_nodes.body
2545 FROM canonical_nodes
2546 JOIN _fathomdb_vector_kinds ON _fathomdb_vector_kinds.kind = canonical_nodes.kind
2547 LEFT JOIN _fathomdb_projection_terminal
2548 ON _fathomdb_projection_terminal.write_cursor = canonical_nodes.write_cursor
2549 WHERE canonical_nodes.write_cursor > ?1
2550 AND _fathomdb_projection_terminal.write_cursor IS NULL
2551 ORDER BY canonical_nodes.write_cursor
2552 LIMIT 32",
2553 )?;
2554 let rows = statement.query_map([cursor], |row| {
2555 Ok(ProjectionJob { cursor: row.get(0)?, kind: row.get(1)?, body: row.get(2)? })
2556 })?;
2557 for row in rows {
2558 let job = row?;
2559 if !in_flight.contains(&job.cursor) {
2560 return Ok(Some(job));
2561 }
2562 }
2563 Ok(None)
2564}
2565
2566fn database_has_pending_projection_work(path: &Path) -> rusqlite::Result<bool> {
2567 let connection = open_runtime_connection(path)?;
2568 let cursor = load_projection_cursor(&connection)?;
2569 connection
2570 .query_row(
2571 "SELECT 1
2572 FROM canonical_nodes
2573 JOIN _fathomdb_vector_kinds ON _fathomdb_vector_kinds.kind = canonical_nodes.kind
2574 LEFT JOIN _fathomdb_projection_terminal
2575 ON _fathomdb_projection_terminal.write_cursor = canonical_nodes.write_cursor
2576 WHERE canonical_nodes.write_cursor > ?1
2577 AND _fathomdb_projection_terminal.write_cursor IS NULL
2578 LIMIT 1",
2579 [cursor],
2580 |_row| Ok(true),
2581 )
2582 .or_else(|err| match err {
2583 rusqlite::Error::QueryReturnedNoRows => Ok(false),
2584 _ => Err(err),
2585 })
2586}
2587
2588struct CanonicalNodeRow {
2589 cursor: u64,
2590 kind: String,
2591 body: String,
2592}
2593
2594fn canonical_node_rows(connection: &Connection) -> rusqlite::Result<Vec<CanonicalNodeRow>> {
2595 let mut statement = connection
2596 .prepare("SELECT write_cursor, kind, body FROM canonical_nodes ORDER BY write_cursor")?;
2597 let rows = statement.query_map([], |row| {
2598 Ok(CanonicalNodeRow {
2599 cursor: row.get::<_, u64>(0)?,
2600 kind: row.get::<_, String>(1)?,
2601 body: row.get::<_, String>(2)?,
2602 })
2603 })?;
2604 rows.collect()
2605}
2606
2607fn hex_encode(bytes: &[u8]) -> String {
2608 let mut out = String::with_capacity(bytes.len() * 2);
2609 for byte in bytes {
2610 out.push(hex_nibble(byte >> 4));
2611 out.push(hex_nibble(byte & 0x0f));
2612 }
2613 out
2614}
2615
2616fn hex_nibble(value: u8) -> char {
2617 match value {
2618 0..=9 => (b'0' + value) as char,
2619 10..=15 => (b'a' + value - 10) as char,
2620 _ => unreachable!(),
2621 }
2622}
2623
2624fn physical_section(connection: &Connection, full: bool) -> Section {
2625 let mut findings = Vec::new();
2626 if let Err(err) = connection.query_row("PRAGMA page_count", [], |row| row.get::<_, i64>(0)) {
2627 findings.push(Finding {
2628 code: "E_CORRUPT_HEADER",
2629 stage: "PhysicalProbe",
2630 locator: locator_from_rusqlite_error(&err),
2631 doc_anchor: "design/recovery.md#header-malformed",
2632 detail: format!("page_count probe failed: {err}"),
2633 });
2634 }
2635 if full {
2636 match collect_integrity_check_findings(connection) {
2637 Ok(rows) => findings.extend(rows),
2638 Err(err) => findings.push(Finding {
2639 code: "E_CORRUPT_INTEGRITY_CHECK",
2640 stage: "IntegrityCheck",
2641 locator: locator_from_rusqlite_error(&err),
2642 doc_anchor: "design/recovery.md#integrity-check-full-findings",
2643 detail: format!("PRAGMA integrity_check failed: {err}"),
2644 }),
2645 }
2646 }
2647 if findings.is_empty() {
2648 Section::Clean
2649 } else {
2650 Section::Findings(findings)
2651 }
2652}
2653
2654fn logical_section(connection: &Connection) -> Section {
2655 let mut findings = Vec::new();
2656 if let Err(err) = connection.query_row("PRAGMA schema_version", [], |row| row.get::<_, i64>(0))
2657 {
2658 findings.push(Finding {
2659 code: "E_CORRUPT_SCHEMA",
2660 stage: "SchemaProbe",
2661 locator: locator_from_rusqlite_error(&err),
2662 doc_anchor: "design/recovery.md#schema-inconsistent",
2663 detail: format!("schema_version probe failed: {err}"),
2664 });
2665 }
2666 match connection.query_row("PRAGMA user_version", [], |row| row.get::<_, u32>(0)) {
2667 Ok(0) => findings.push(Finding {
2668 code: "E_CORRUPT_SCHEMA",
2669 stage: "SchemaProbe",
2670 locator: CorruptionLocator::MigrationStep { from: 0, to: 0 },
2671 doc_anchor: "design/recovery.md#schema-inconsistent",
2672 detail: "user_version is zero".to_string(),
2673 }),
2674 Ok(_) => {}
2675 Err(err) => findings.push(Finding {
2676 code: "E_CORRUPT_SCHEMA",
2677 stage: "SchemaProbe",
2678 locator: locator_from_rusqlite_error(&err),
2679 doc_anchor: "design/recovery.md#schema-inconsistent",
2680 detail: format!("user_version probe failed: {err}"),
2681 }),
2682 }
2683 if findings.is_empty() {
2684 Section::Clean
2685 } else {
2686 Section::Findings(findings)
2687 }
2688}
2689
2690fn semantic_section(connection: &Connection) -> Section {
2691 match load_default_profile(connection) {
2692 Ok(_) => Section::Clean,
2693 Err(rusqlite::Error::QueryReturnedNoRows) => Section::Findings(vec![Finding {
2694 code: "E_CORRUPT_EMBEDDER_IDENTITY",
2695 stage: "EmbedderIdentity",
2696 locator: CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: 0 },
2697 doc_anchor: "design/recovery.md#embedder-identity-drift",
2698 detail: "default embedder profile row is missing".to_string(),
2699 }]),
2700 Err(err) => Section::Findings(vec![Finding {
2701 code: "E_CORRUPT_EMBEDDER_IDENTITY",
2702 stage: "EmbedderIdentity",
2703 locator: locator_from_rusqlite_error(&err),
2704 doc_anchor: "design/recovery.md#embedder-identity-drift",
2705 detail: format!("default embedder profile probe failed: {err}"),
2706 }]),
2707 }
2708}
2709
2710fn collect_integrity_check_findings(connection: &Connection) -> rusqlite::Result<Vec<Finding>> {
2711 let mut statement = connection.prepare("PRAGMA integrity_check")?;
2712 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
2713 let mut findings = Vec::new();
2714 for row in rows {
2715 let message = row?;
2716 if message == "ok" {
2717 continue;
2718 }
2719 findings.push(Finding {
2720 code: "E_CORRUPT_INTEGRITY_CHECK",
2721 stage: "IntegrityCheck",
2722 locator: CorruptionLocator::OpaqueSqliteError {
2723 sqlite_extended_code: rusqlite::ffi::SQLITE_CORRUPT,
2724 },
2725 doc_anchor: "design/recovery.md#integrity-check-full-findings",
2726 detail: message,
2727 });
2728 }
2729 Ok(findings)
2730}
2731
2732fn locator_from_rusqlite_error(err: &rusqlite::Error) -> CorruptionLocator {
2733 let extended = err.sqlite_error().map(|inner| inner.extended_code).unwrap_or(0);
2734 CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: extended }
2735}
2736
2737fn open_runtime_connection(path: &Path) -> rusqlite::Result<Connection> {
2738 let connection = Connection::open(path)?;
2739 connection.pragma_update(None, "journal_mode", "WAL")?;
2740 Ok(connection)
2741}
2742
2743fn load_projection_cursor(connection: &Connection) -> rusqlite::Result<u64> {
2744 connection
2745 .query_row(
2746 "SELECT value FROM _fathomdb_open_state WHERE key = ?1",
2747 [PROJECTION_CURSOR_KEY],
2748 |row| row.get::<_, String>(0),
2749 )
2750 .map(|value| value.parse::<u64>().unwrap_or(0))
2751 .or_else(|err| match err {
2752 rusqlite::Error::QueryReturnedNoRows => Ok(0),
2753 _ => Err(err),
2754 })
2755}
2756
2757fn store_projection_cursor(connection: &Connection, cursor: u64) -> rusqlite::Result<()> {
2758 connection.execute(
2759 "INSERT INTO _fathomdb_open_state(key, value) VALUES(?1, ?2)
2760 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
2761 params![PROJECTION_CURSOR_KEY, cursor.to_string()],
2762 )?;
2763 Ok(())
2764}
2765
2766fn record_projection_terminal(
2767 connection: &Connection,
2768 cursor: u64,
2769 state: &str,
2770) -> rusqlite::Result<()> {
2771 connection.execute(
2772 "INSERT OR IGNORE INTO _fathomdb_projection_terminal(write_cursor, state) VALUES(?1, ?2)",
2773 params![cursor, state],
2774 )?;
2775 Ok(())
2776}
2777
2778fn terminal_state_for_cursor(
2779 connection: &Connection,
2780 cursor: u64,
2781) -> rusqlite::Result<Option<String>> {
2782 connection
2783 .query_row(
2784 "SELECT state FROM _fathomdb_projection_terminal WHERE write_cursor = ?1",
2785 [cursor],
2786 |row| row.get::<_, String>(0),
2787 )
2788 .map(Some)
2789 .or_else(|err| match err {
2790 rusqlite::Error::QueryReturnedNoRows => Ok(None),
2791 _ => Err(err),
2792 })
2793}
2794
2795fn advance_projection_cursor(connection: &Connection) -> rusqlite::Result<u64> {
2796 let mut cursor = load_projection_cursor(connection)?;
2797 loop {
2798 let next = cursor.saturating_add(1);
2799 if terminal_state_for_cursor(connection, next)?.is_some() {
2800 cursor = next;
2801 } else {
2802 break;
2803 }
2804 }
2805 store_projection_cursor(connection, cursor)?;
2806 Ok(cursor)
2807}
2808
2809fn commit_projection_outcomes(
2810 connection: &mut Connection,
2811 outcomes: &[ProjectionOutcome],
2812) -> rusqlite::Result<()> {
2813 let tx = connection.transaction()?;
2814 for outcome in outcomes {
2815 match outcome {
2816 ProjectionOutcome::Success { cursor, kind, blob } => {
2817 if terminal_state_for_cursor(&tx, *cursor)?.is_some() {
2818 continue;
2819 }
2820 tx.execute(
2821 "INSERT OR IGNORE INTO _fathomdb_vector_rows(rowid, kind, write_cursor) VALUES(?1, ?2, ?3)",
2822 params![cursor, kind, cursor],
2823 )?;
2824 tx.execute(
2825 "INSERT OR IGNORE INTO vector_default(rowid, embedding) VALUES(?1, ?2)",
2826 params![cursor, blob],
2827 )?;
2828 record_projection_terminal(&tx, *cursor, "up_to_date")?;
2829 }
2830 ProjectionOutcome::Failure { cursor, failure_code } => {
2831 if terminal_state_for_cursor(&tx, *cursor)?.is_some() {
2832 continue;
2833 }
2834 let existing: u64 = tx.query_row(
2835 "SELECT COUNT(*) FROM operational_mutations
2836 WHERE collection_name = 'projection_failures'
2837 AND json_extract(payload_json, '$.write_cursor') = ?1",
2838 [cursor],
2839 |row| row.get(0),
2840 )?;
2841 if existing == 0 {
2842 let payload = format!(
2843 r#"{{"write_cursor":{cursor},"failure_code":"{failure_code}","recorded_at":0}}"#
2844 );
2845 tx.execute(
2846 "INSERT INTO operational_mutations(
2847 collection_name, record_key, op_kind, payload_json, schema_id, write_cursor
2848 ) VALUES('projection_failures', ?1, 'append', ?2, NULL, ?3)",
2849 params![cursor.to_string(), payload, cursor],
2850 )?;
2851 }
2852 record_projection_terminal(&tx, *cursor, "failed")?;
2853 }
2854 }
2855 }
2856 advance_projection_cursor(&tx)?;
2857 tx.commit()
2858}
2859
2860fn enforce_provenance_retention(connection: &Connection, cap: u64) -> rusqlite::Result<()> {
2861 if cap == 0 {
2862 return Ok(());
2863 }
2864 let slack = cap.max(20) / 20;
2865 let upper = cap.saturating_add(slack.max(1));
2866 let count: u64 =
2867 connection.query_row("SELECT COUNT(*) FROM operational_mutations", [], |row| row.get(0))?;
2868 if count <= upper {
2869 return Ok(());
2870 }
2871 let to_delete = count.saturating_sub(cap);
2872 connection.execute(
2873 "DELETE FROM operational_mutations
2874 WHERE id IN (
2875 SELECT id FROM operational_mutations
2876 ORDER BY id
2877 LIMIT ?1
2878 )",
2879 [to_delete],
2880 )?;
2881 Ok(())
2882}
2883
2884fn projection_status(
2885 connection: &Connection,
2886 kind: &str,
2887) -> Result<lifecycle::ProjectionStatus, EngineError> {
2888 let latest = connection
2889 .query_row(
2890 "SELECT COALESCE(MAX(write_cursor), 0) FROM canonical_nodes WHERE kind = ?1",
2891 [kind],
2892 |row| row.get::<_, u64>(0),
2893 )
2894 .map_err(|_| EngineError::Storage)?;
2895 if latest == 0 {
2896 return Ok(lifecycle::ProjectionStatus::UpToDate);
2897 }
2898 let pending: u64 = connection
2899 .query_row(
2900 "SELECT COUNT(*)
2901 FROM canonical_nodes
2902 LEFT JOIN _fathomdb_projection_terminal
2903 ON _fathomdb_projection_terminal.write_cursor = canonical_nodes.write_cursor
2904 WHERE canonical_nodes.kind = ?1
2905 AND _fathomdb_projection_terminal.write_cursor IS NULL",
2906 [kind],
2907 |row| row.get(0),
2908 )
2909 .map_err(|_| EngineError::Storage)?;
2910 if pending > 0 {
2911 return Ok(lifecycle::ProjectionStatus::Pending);
2912 }
2913 match terminal_state_for_cursor(connection, latest).map_err(|_| EngineError::Storage)? {
2914 Some(state) if state == "failed" => Ok(lifecycle::ProjectionStatus::Failed),
2915 _ => Ok(lifecycle::ProjectionStatus::UpToDate),
2916 }
2917}
2918
2919fn canonical_database_path(path: &Path) -> Result<PathBuf, EngineOpenError> {
2920 let parent = path
2921 .parent()
2922 .filter(|parent| !parent.as_os_str().is_empty())
2923 .unwrap_or_else(|| Path::new("."));
2924 let canonical_parent = parent.canonicalize().map_err(|_| EngineOpenError::Io {
2925 message: "database parent directory is not accessible".to_string(),
2926 })?;
2927 let file_name = path.file_name().ok_or_else(|| EngineOpenError::Io {
2928 message: "database path has no file name".to_string(),
2929 })?;
2930
2931 Ok(canonical_parent.join(file_name))
2932}
2933
2934fn acquire_lock(path: &Path) -> Result<File, EngineOpenError> {
2935 let lock_path = lock_path(path);
2936 let mut options = OpenOptions::new();
2937 options.read(true).write(true).create(true);
2938 #[cfg(unix)]
2939 options.mode(0o600);
2940
2941 let mut file = options.open(&lock_path).map_err(|_| EngineOpenError::Io {
2942 message: "could not open database lock file".to_string(),
2943 })?;
2944
2945 match file.try_lock() {
2946 Ok(()) => {
2947 let pid = std::process::id().to_string();
2948 let _ = file.set_len(0);
2949 let _ = file.seek(SeekFrom::Start(0));
2950 let _ = file.write_all(pid.as_bytes());
2951 Ok(file)
2952 }
2953 Err(std::fs::TryLockError::WouldBlock) => {
2954 Err(EngineOpenError::DatabaseLocked { holder_pid: read_holder_pid(&lock_path) })
2955 }
2956 Err(_) => {
2957 Err(EngineOpenError::Io { message: "could not acquire database lock".to_string() })
2958 }
2959 }
2960}
2961
2962fn lock_path(path: &Path) -> PathBuf {
2963 let mut lock_path = path.as_os_str().to_os_string();
2964 lock_path.push(LOCK_SUFFIX);
2965 PathBuf::from(lock_path)
2966}
2967
2968fn read_holder_pid(path: &Path) -> Option<u32> {
2969 std::fs::read_to_string(path).ok()?.trim().parse().ok()
2970}
2971
2972fn map_migration_error(err: SchemaMigrationError) -> EngineOpenError {
2973 match err {
2974 SchemaMigrationError::IncompatibleSchemaVersion { seen, supported } => {
2975 EngineOpenError::IncompatibleSchemaVersion { seen, supported }
2976 }
2977 SchemaMigrationError::MigrationError(report) => EngineOpenError::MigrationError {
2978 schema_version_before: report.schema_version_before,
2979 schema_version_current: report.schema_version_current,
2980 step_id: report.migration_steps.last().map_or(0, |step| step.step_id),
2981 },
2982 SchemaMigrationError::Storage { message } => {
2983 EngineOpenError::Io { message: message.to_string() }
2984 }
2985 }
2986}
2987
2988fn register_sqlite_vec_extension() {
2989 static REGISTER: Once = Once::new();
2990 REGISTER.call_once(|| unsafe {
2991 let entrypoint: unsafe extern "C" fn(
2992 *mut rusqlite::ffi::sqlite3,
2993 *mut *const std::os::raw::c_char,
2994 *const rusqlite::ffi::sqlite3_api_routines,
2995 ) -> std::os::raw::c_int = std::mem::transmute(sqlite3_vec_init as *const ());
2996 rusqlite::ffi::sqlite3_auto_extension(Some(entrypoint));
2997 });
2998}
2999
3000fn probe_open_integrity(connection: &Connection) -> Result<(), EngineOpenError> {
3001 connection
3006 .query_row("SELECT COUNT(*) FROM sqlite_schema", [], |row| row.get::<_, i64>(0))
3007 .map(|_| ())
3008 .map_err(|err| map_open_sqlite_error(err, OpenStage::SchemaProbe))
3009}
3010
3011fn probe_database_header(connection: &Connection) -> Result<(), EngineOpenError> {
3012 connection
3013 .query_row("PRAGMA application_id", [], |row| row.get::<_, i64>(0))
3014 .map(|_| ())
3015 .map_err(|err| map_open_sqlite_error(err, OpenStage::HeaderProbe))
3016}
3017
3018fn probe_wal_sidecar(db_path: &Path) -> Result<(), EngineOpenError> {
3025 let mut wal_path = db_path.as_os_str().to_owned();
3026 wal_path.push("-wal");
3027 let wal_path = PathBuf::from(wal_path);
3028 use std::io::Read;
3036 let mut file = match std::fs::File::open(&wal_path) {
3037 Ok(file) => file,
3038 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
3039 Err(_) => return Ok(()),
3040 };
3041 let mut bytes = [0u8; 32];
3042 if file.read_exact(&mut bytes).is_err() {
3043 return Ok(());
3046 }
3047 let magic = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
3048 let page_size = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
3049 const WAL_MAGIC_MASK: u32 = 0xFFFF_FFFE;
3053 const WAL_MAGIC: u32 = 0x377F_0682;
3054 const SQLITE_MAX_PAGE_SIZE: u32 = 65536;
3055 let magic_ok = (magic & WAL_MAGIC_MASK) == WAL_MAGIC;
3056 let page_size_ok =
3057 page_size.is_power_of_two() && (512..=SQLITE_MAX_PAGE_SIZE).contains(&page_size);
3058 if magic_ok && page_size_ok {
3059 return Ok(());
3060 }
3061 Err(EngineOpenError::Corruption(CorruptionDetail {
3062 kind: CorruptionKind::WalReplayFailure,
3063 stage: OpenStage::WalReplay,
3064 locator: CorruptionLocator::FileOffset { offset: if !magic_ok { 0 } else { 8 } },
3065 recovery_hint: RecoveryHint {
3066 code: "E_CORRUPT_WAL_REPLAY",
3067 doc_anchor: "design/recovery.md#wal-replay-failures",
3068 },
3069 }))
3070}
3071
3072fn reject_legacy_shape(connection: &Connection) -> Result<(), EngineOpenError> {
3073 let has_legacy_table = table_exists(connection, "fathom_nodes")
3074 || table_exists(connection, "fathom_edges")
3075 || table_exists(connection, "fathom_chunks");
3076 if !has_legacy_table {
3077 return Ok(());
3078 }
3079
3080 let seen =
3081 connection.query_row("PRAGMA user_version", [], |row| row.get::<_, u32>(0)).unwrap_or(0);
3082 Err(EngineOpenError::IncompatibleSchemaVersion { seen, supported: SCHEMA_VERSION })
3083}
3084
3085fn table_exists(connection: &Connection, table: &str) -> bool {
3086 connection
3087 .query_row(
3088 "SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?1",
3089 [table],
3090 |_row| Ok(()),
3091 )
3092 .is_ok()
3093}
3094
3095fn read_schema_objects(
3096 connection: &Connection,
3097 obj_type: &str,
3098) -> Result<Vec<SchemaObject>, EngineError> {
3099 let mut stmt = connection
3100 .prepare(
3101 "SELECT name, sql FROM sqlite_schema
3102 WHERE type = ?1 AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL
3103 ORDER BY name",
3104 )
3105 .map_err(|_| EngineError::Storage)?;
3106 let rows = stmt
3107 .query_map([obj_type], |row| {
3108 Ok(SchemaObject { name: row.get::<_, String>(0)?, sql: row.get::<_, String>(1)? })
3109 })
3110 .map_err(|_| EngineError::Storage)?;
3111 let mut out = Vec::new();
3112 for row in rows {
3113 out.push(row.map_err(|_| EngineError::Storage)?);
3114 }
3115 Ok(out)
3116}
3117
3118fn order_canonical_first(mut objects: Vec<SchemaObject>) -> Vec<SchemaObject> {
3119 let mut canonical: Vec<SchemaObject> = Vec::new();
3120 for name in CANONICAL_TABLES {
3121 if let Some(pos) = objects.iter().position(|o| o.name == *name) {
3122 canonical.push(objects.remove(pos));
3123 }
3124 }
3125 canonical.extend(objects);
3126 canonical
3127}
3128
3129fn load_default_profile(connection: &Connection) -> rusqlite::Result<EmbedderIdentity> {
3130 connection.query_row(
3131 "SELECT name, revision, dimension FROM _fathomdb_embedder_profiles WHERE profile = ?1",
3132 [DEFAULT_VECTOR_PROFILE],
3133 |row| {
3134 Ok(EmbedderIdentity::new(
3135 row.get::<_, String>(0)?,
3136 row.get::<_, String>(1)?,
3137 row.get::<_, u32>(2)?,
3138 ))
3139 },
3140 )
3141}
3142
3143fn default_profile_dimension(connection: &Connection) -> Result<u32, EngineError> {
3144 load_default_profile(connection)
3145 .map(|identity| identity.dimension)
3146 .map_err(|_| EngineError::Storage)
3147}
3148
3149fn kind_is_vector_indexed(connection: &Connection, kind: &str) -> Result<bool, EngineError> {
3150 connection
3151 .query_row("SELECT 1 FROM _fathomdb_vector_kinds WHERE kind = ?1", [kind], |_row| Ok(()))
3152 .map(|_| true)
3153 .or_else(|err| match err {
3154 rusqlite::Error::QueryReturnedNoRows => Ok(false),
3155 _ => Err(EngineError::Storage),
3156 })
3157}
3158
3159fn ensure_vector_partition(connection: &Connection, dimension: u32) -> rusqlite::Result<()> {
3160 let sql = format!(
3161 "CREATE VIRTUAL TABLE IF NOT EXISTS {DEFAULT_VECTOR_PARTITION} USING vec0(embedding float[{dimension}])"
3162 );
3163 connection.execute_batch(&sql)
3164}
3165
3166fn encode_vector_blob(vector: &[f32]) -> Vec<u8> {
3167 vector.iter().flat_map(|value| value.to_le_bytes()).collect()
3168}
3169
3170fn map_runtime_embedder_error(err: RuntimeEmbedderError) -> EngineError {
3171 match err {
3172 RuntimeEmbedderError::Failed { .. } | RuntimeEmbedderError::Timeout => {
3173 EngineError::Embedder
3174 }
3175 }
3176}
3177
3178fn default_embedder_identity() -> EmbedderIdentity {
3179 EmbedderIdentity::new(
3180 DEFAULT_EMBEDDER_NAME,
3181 DEFAULT_EMBEDDER_REVISION,
3182 DEFAULT_EMBEDDER_DIMENSION,
3183 )
3184}
3185
3186fn check_embedder_profile(
3187 connection: &Connection,
3188 supplied: &EmbedderIdentity,
3189) -> Result<(), EngineOpenError> {
3190 let mut statement = match connection.prepare(
3191 "SELECT name, revision, dimension FROM _fathomdb_embedder_profiles WHERE profile = 'default'",
3192 ) {
3193 Ok(statement) => statement,
3194 Err(_) => return Ok(()),
3195 };
3196 let mut rows = statement.query([]).map_err(|_| {
3197 EngineOpenError::Corruption(CorruptionDetail {
3198 kind: CorruptionKind::EmbedderIdentityDrift,
3199 stage: OpenStage::EmbedderIdentity,
3200 locator: CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: 0 },
3201 recovery_hint: RecoveryHint {
3202 code: "E_CORRUPT_EMBEDDER_IDENTITY",
3203 doc_anchor: "design/recovery.md#embedder-identity-drift",
3204 },
3205 })
3206 })?;
3207
3208 let Some(row) = rows.next().map_err(|_| {
3209 EngineOpenError::Corruption(CorruptionDetail {
3210 kind: CorruptionKind::EmbedderIdentityDrift,
3211 stage: OpenStage::EmbedderIdentity,
3212 locator: CorruptionLocator::OpaqueSqliteError { sqlite_extended_code: 0 },
3213 recovery_hint: RecoveryHint {
3214 code: "E_CORRUPT_EMBEDDER_IDENTITY",
3215 doc_anchor: "design/recovery.md#embedder-identity-drift",
3216 },
3217 })
3218 })?
3219 else {
3220 connection
3221 .execute(
3222 "INSERT INTO _fathomdb_embedder_profiles(profile, name, revision, dimension)
3223 VALUES(?1, ?2, ?3, ?4)",
3224 params![
3225 DEFAULT_VECTOR_PROFILE,
3226 supplied.name,
3227 supplied.revision,
3228 supplied.dimension
3229 ],
3230 )
3231 .map_err(|_| EngineOpenError::Io {
3232 message: "could not persist embedder profile".to_string(),
3233 })?;
3234 return Ok(());
3235 };
3236
3237 let stored_name = row.get::<_, String>(0).map_err(|_| {
3238 EngineOpenError::Corruption(CorruptionDetail {
3239 kind: CorruptionKind::EmbedderIdentityDrift,
3240 stage: OpenStage::EmbedderIdentity,
3241 locator: CorruptionLocator::TableRow { table: "_fathomdb_embedder_profiles", rowid: 0 },
3242 recovery_hint: RecoveryHint {
3243 code: "E_CORRUPT_EMBEDDER_IDENTITY",
3244 doc_anchor: "design/recovery.md#embedder-identity-drift",
3245 },
3246 })
3247 })?;
3248 let stored_revision = row.get::<_, String>(1).map_err(|_| {
3249 EngineOpenError::Corruption(CorruptionDetail {
3250 kind: CorruptionKind::EmbedderIdentityDrift,
3251 stage: OpenStage::EmbedderIdentity,
3252 locator: CorruptionLocator::TableRow { table: "_fathomdb_embedder_profiles", rowid: 0 },
3253 recovery_hint: RecoveryHint {
3254 code: "E_CORRUPT_EMBEDDER_IDENTITY",
3255 doc_anchor: "design/recovery.md#embedder-identity-drift",
3256 },
3257 })
3258 })?;
3259 let dimension = row.get::<_, u32>(2).map_err(|_| {
3260 EngineOpenError::Corruption(CorruptionDetail {
3261 kind: CorruptionKind::EmbedderIdentityDrift,
3262 stage: OpenStage::EmbedderIdentity,
3263 locator: CorruptionLocator::TableRow { table: "_fathomdb_embedder_profiles", rowid: 0 },
3264 recovery_hint: RecoveryHint {
3265 code: "E_CORRUPT_EMBEDDER_IDENTITY",
3266 doc_anchor: "design/recovery.md#embedder-identity-drift",
3267 },
3268 })
3269 })?;
3270
3271 let stored = EmbedderIdentity::new(stored_name, stored_revision, dimension);
3272
3273 if stored.name != supplied.name || stored.revision != supplied.revision {
3274 return Err(EngineOpenError::EmbedderIdentityMismatch {
3275 stored,
3276 supplied: supplied.clone(),
3277 });
3278 }
3279 if dimension != supplied.dimension {
3280 return Err(EngineOpenError::EmbedderDimensionMismatch {
3281 stored: dimension,
3282 supplied: supplied.dimension,
3283 });
3284 }
3285
3286 Ok(())
3287}
3288
3289#[derive(Clone, Debug, Eq, PartialEq)]
3290enum WritePlan {
3291 Node,
3292 Edge,
3293 AppendOnlyLog,
3294 LatestState,
3295 AdminSchema,
3296}
3297
3298fn validate_batch(
3299 connection: &Connection,
3300 batch: &[PreparedWrite],
3301) -> Result<Vec<WritePlan>, EngineError> {
3302 batch.iter().map(|write| validate_write(connection, write)).collect()
3303}
3304
3305fn collect_projection_jobs(
3306 connection: &Connection,
3307 batch: &[PreparedWrite],
3308) -> Result<Vec<ProjectionJob>, EngineError> {
3309 let mut jobs = Vec::new();
3310 for write in batch {
3311 if let PreparedWrite::Node { kind, body, .. } = write {
3312 if kind_is_vector_indexed(connection, kind)? {
3313 jobs.push(ProjectionJob { cursor: 0, kind: kind.clone(), body: body.clone() });
3314 }
3315 }
3316 }
3317 Ok(jobs)
3318}
3319
3320fn validate_write(
3321 connection: &Connection,
3322 write: &PreparedWrite,
3323) -> Result<WritePlan, EngineError> {
3324 match write {
3325 PreparedWrite::Node { kind, body, source_id } => {
3326 if kind.trim().is_empty() || body.trim().is_empty() {
3327 return Err(EngineError::WriteValidation);
3328 }
3329 if let Some(source_id) = source_id {
3330 if source_id.is_empty() {
3331 return Err(EngineError::WriteValidation);
3332 }
3333 }
3334 Ok(WritePlan::Node)
3335 }
3336 PreparedWrite::Edge { kind, from, to, source_id } => {
3337 if kind.trim().is_empty() || from.trim().is_empty() || to.trim().is_empty() {
3338 return Err(EngineError::WriteValidation);
3339 }
3340 if let Some(source_id) = source_id {
3341 if source_id.is_empty() {
3342 return Err(EngineError::WriteValidation);
3343 }
3344 }
3345 Ok(WritePlan::Edge)
3346 }
3347 PreparedWrite::AdminSchema { name, kind, schema_json, retention_json } => {
3348 if name.trim().is_empty()
3349 || !matches!(kind.as_str(), "append_only_log" | "latest_state")
3350 || serde_json::from_str::<Value>(schema_json).is_err()
3351 || serde_json::from_str::<Value>(retention_json).is_err()
3352 || contains_external_ref(schema_json)
3353 {
3354 return Err(EngineError::SchemaValidation);
3355 }
3356 Ok(WritePlan::AdminSchema)
3357 }
3358 PreparedWrite::OpStore { collection, record_key, schema_id, body } => {
3359 if collection.trim().is_empty() || record_key.trim().is_empty() {
3360 return Err(EngineError::WriteValidation);
3361 }
3362 let (kind, schema_json) = collection_metadata(connection, collection)?;
3363 if let Some(schema_id) = schema_id {
3364 if schema_id != collection {
3365 return Err(EngineError::SchemaValidation);
3366 }
3367 validate_payload(&schema_json, body)?;
3368 } else if serde_json::from_str::<Value>(body).is_err() {
3369 return Err(EngineError::SchemaValidation);
3370 }
3371
3372 match kind.as_str() {
3373 "append_only_log" => Ok(WritePlan::AppendOnlyLog),
3374 "latest_state" => Ok(WritePlan::LatestState),
3375 _ => Err(EngineError::OpStore),
3376 }
3377 }
3378 }
3379}
3380
3381fn collection_metadata(
3382 connection: &Connection,
3383 collection: &str,
3384) -> Result<(String, String), EngineError> {
3385 connection
3386 .query_row(
3387 "SELECT kind, schema_json FROM operational_collections WHERE name = ?1",
3388 [collection],
3389 |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
3390 )
3391 .map_err(|_| EngineError::OpStore)
3392}
3393
3394fn validate_payload(schema_json: &str, body: &str) -> Result<(), EngineError> {
3395 let schema =
3396 serde_json::from_str::<Value>(schema_json).map_err(|_| EngineError::SchemaValidation)?;
3397 let payload = serde_json::from_str::<Value>(body).map_err(|_| EngineError::SchemaValidation)?;
3398
3399 let compiled = JSONSchema::compile(&schema).map_err(|_| EngineError::SchemaValidation)?;
3400 compiled.validate(&payload).map_err(|_| EngineError::SchemaValidation)?;
3401
3402 Ok(())
3403}
3404
3405fn contains_external_ref(schema_json: &str) -> bool {
3406 let Ok(value) = serde_json::from_str::<Value>(schema_json) else {
3407 return false;
3408 };
3409 value_contains_external_ref(&value)
3410}
3411
3412fn value_contains_external_ref(value: &Value) -> bool {
3413 match value {
3414 Value::Object(object) => object.iter().any(|(key, value)| {
3415 if key == "$ref" {
3416 return value.as_str().is_some_and(|uri| !uri.starts_with('#'));
3417 }
3418 value_contains_external_ref(value)
3419 }),
3420 Value::Array(values) => values.iter().any(value_contains_external_ref),
3421 _ => false,
3422 }
3423}
3424
3425fn commit_batch(
3426 connection: &mut Connection,
3427 batch: &[PreparedWrite],
3428 plans: &[WritePlan],
3429 cursor: u64,
3430 pending_projection: bool,
3431 provenance_row_cap: u64,
3432) -> rusqlite::Result<()> {
3433 let tx = connection.transaction()?;
3434
3435 for (write, plan) in batch.iter().zip(plans) {
3436 match (write, plan) {
3437 (PreparedWrite::Node { kind, body, source_id }, WritePlan::Node) => {
3438 tx.execute(
3439 "INSERT INTO canonical_nodes(write_cursor, kind, body, source_id)
3440 VALUES(?1, ?2, ?3, ?4)",
3441 params![cursor, kind, body, source_id],
3442 )?;
3443 tx.execute(
3444 "INSERT INTO search_index(body, kind, write_cursor) VALUES(?1, ?2, ?3)",
3445 params![body, kind, cursor],
3446 )?;
3447 if kind_is_vector_indexed(&tx, kind).unwrap_or(false) {
3448 tx.execute(
3449 "INSERT INTO _fathomdb_projection_state(kind, last_enqueued_cursor, updated_at)
3450 VALUES(?1, ?2, 0)
3451 ON CONFLICT(kind) DO UPDATE SET last_enqueued_cursor = excluded.last_enqueued_cursor",
3452 params![kind, cursor],
3453 )?;
3454 }
3455 }
3456 (PreparedWrite::Edge { kind, from, to, source_id }, WritePlan::Edge) => {
3457 tx.execute(
3458 "INSERT INTO canonical_edges(write_cursor, kind, from_id, to_id, source_id)
3459 VALUES(?1, ?2, ?3, ?4, ?5)",
3460 params![cursor, kind, from, to, source_id],
3461 )?;
3462 }
3463 (
3464 PreparedWrite::AdminSchema { name, kind, schema_json, retention_json },
3465 WritePlan::AdminSchema,
3466 ) => {
3467 tx.execute(
3468 "INSERT INTO operational_collections(
3469 name, kind, schema_json, retention_json, format_version, created_at
3470 ) VALUES(?1, ?2, ?3, ?4, 1, 0)
3471 ON CONFLICT(name) DO UPDATE SET
3472 schema_json = excluded.schema_json,
3473 retention_json = excluded.retention_json",
3474 params![name, kind, schema_json, retention_json],
3475 )?;
3476 }
3477 (
3478 PreparedWrite::OpStore { collection, record_key, schema_id, body },
3479 WritePlan::AppendOnlyLog,
3480 ) => {
3481 tx.execute(
3482 "INSERT INTO operational_mutations(
3483 collection_name, record_key, op_kind, payload_json, schema_id, write_cursor
3484 ) VALUES(?1, ?2, 'append', ?3, ?4, ?5)",
3485 params![collection, record_key, body, schema_id, cursor],
3486 )?;
3487 }
3488 (
3489 PreparedWrite::OpStore { collection, record_key, schema_id, body },
3490 WritePlan::LatestState,
3491 ) => {
3492 tx.execute(
3493 "INSERT INTO operational_state(
3494 collection_name, record_key, payload_json, schema_id, write_cursor
3495 ) VALUES(?1, ?2, ?3, ?4, ?5)
3496 ON CONFLICT(collection_name, record_key) DO UPDATE SET
3497 payload_json = excluded.payload_json,
3498 schema_id = excluded.schema_id,
3499 write_cursor = excluded.write_cursor",
3500 params![collection, record_key, body, schema_id, cursor],
3501 )?;
3502 }
3503 _ => return Err(rusqlite::Error::InvalidQuery),
3504 }
3505 }
3506
3507 if !pending_projection {
3508 record_projection_terminal(&tx, cursor, "up_to_date")?;
3509 }
3510 enforce_provenance_retention(&tx, provenance_row_cap)?;
3511 advance_projection_cursor(&tx)?;
3512
3513 tx.commit()
3514}
3515
3516fn load_next_cursor(connection: &Connection) -> u64 {
3517 let nodes = max_cursor(connection, "canonical_nodes").unwrap_or(0);
3518 let edges = max_cursor(connection, "canonical_edges").unwrap_or(0);
3519 let mutations = max_cursor(connection, "operational_mutations").unwrap_or(0);
3520 let state = max_cursor(connection, "operational_state").unwrap_or(0);
3521 nodes.max(edges).max(mutations).max(state)
3522}
3523
3524fn max_cursor(connection: &Connection, table: &str) -> rusqlite::Result<u64> {
3525 let sql = format!("SELECT COALESCE(MAX(write_cursor), 0) FROM {table}");
3526 connection.query_row(&sql, [], |row| row.get::<_, u64>(0))
3527}
3528
3529fn sqlite_extended_code_name(err: &rusqlite::Error) -> Option<&'static str> {
3550 let sqlite_error = err.sqlite_error()?;
3551 let extended = sqlite_error.extended_code;
3552 Some(match extended {
3553 rusqlite::ffi::SQLITE_SCHEMA => "SQLITE_SCHEMA",
3554 rusqlite::ffi::SQLITE_BUSY => "SQLITE_BUSY",
3555 rusqlite::ffi::SQLITE_LOCKED => "SQLITE_LOCKED",
3556 rusqlite::ffi::SQLITE_CORRUPT => "SQLITE_CORRUPT",
3557 rusqlite::ffi::SQLITE_NOTADB => "SQLITE_NOTADB",
3558 rusqlite::ffi::SQLITE_IOERR => "SQLITE_IOERR",
3559 rusqlite::ffi::SQLITE_FULL => "SQLITE_FULL",
3560 rusqlite::ffi::SQLITE_READONLY => "SQLITE_READONLY",
3561 rusqlite::ffi::SQLITE_CONSTRAINT => "SQLITE_CONSTRAINT",
3562 rusqlite::ffi::SQLITE_MISUSE => "SQLITE_MISUSE",
3563 rusqlite::ffi::SQLITE_INTERRUPT => "SQLITE_INTERRUPT",
3564 rusqlite::ffi::SQLITE_NOMEM => "SQLITE_NOMEM",
3565 rusqlite::ffi::SQLITE_PERM => "SQLITE_PERM",
3566 rusqlite::ffi::SQLITE_ABORT => "SQLITE_ABORT",
3567 rusqlite::ffi::SQLITE_PROTOCOL => "SQLITE_PROTOCOL",
3568 rusqlite::ffi::SQLITE_RANGE => "SQLITE_RANGE",
3569 rusqlite::ffi::SQLITE_TOOBIG => "SQLITE_TOOBIG",
3570 rusqlite::ffi::SQLITE_MISMATCH => "SQLITE_MISMATCH",
3571 rusqlite::ffi::SQLITE_AUTH => "SQLITE_AUTH",
3572 rusqlite::ffi::SQLITE_NOTFOUND => "SQLITE_NOTFOUND",
3573 rusqlite::ffi::SQLITE_CANTOPEN => "SQLITE_CANTOPEN",
3574 _ => "SQLITE_UNKNOWN",
3575 })
3576}
3577
3578fn sqlite_extended_code_name_from_int(extended: i32) -> &'static str {
3579 match extended {
3580 rusqlite::ffi::SQLITE_SCHEMA => "SQLITE_SCHEMA",
3581 rusqlite::ffi::SQLITE_BUSY => "SQLITE_BUSY",
3582 rusqlite::ffi::SQLITE_LOCKED => "SQLITE_LOCKED",
3583 rusqlite::ffi::SQLITE_CORRUPT => "SQLITE_CORRUPT",
3584 rusqlite::ffi::SQLITE_NOTADB => "SQLITE_NOTADB",
3585 rusqlite::ffi::SQLITE_IOERR => "SQLITE_IOERR",
3586 rusqlite::ffi::SQLITE_FULL => "SQLITE_FULL",
3587 rusqlite::ffi::SQLITE_READONLY => "SQLITE_READONLY",
3588 rusqlite::ffi::SQLITE_CONSTRAINT => "SQLITE_CONSTRAINT",
3589 rusqlite::ffi::SQLITE_MISUSE => "SQLITE_MISUSE",
3590 rusqlite::ffi::SQLITE_INTERRUPT => "SQLITE_INTERRUPT",
3591 rusqlite::ffi::SQLITE_NOMEM => "SQLITE_NOMEM",
3592 rusqlite::ffi::SQLITE_PERM => "SQLITE_PERM",
3593 rusqlite::ffi::SQLITE_ABORT => "SQLITE_ABORT",
3594 rusqlite::ffi::SQLITE_PROTOCOL => "SQLITE_PROTOCOL",
3595 rusqlite::ffi::SQLITE_RANGE => "SQLITE_RANGE",
3596 rusqlite::ffi::SQLITE_TOOBIG => "SQLITE_TOOBIG",
3597 rusqlite::ffi::SQLITE_MISMATCH => "SQLITE_MISMATCH",
3598 rusqlite::ffi::SQLITE_AUTH => "SQLITE_AUTH",
3599 rusqlite::ffi::SQLITE_NOTFOUND => "SQLITE_NOTFOUND",
3600 rusqlite::ffi::SQLITE_CANTOPEN => "SQLITE_CANTOPEN",
3601 _ => "SQLITE_UNKNOWN",
3602 }
3603}
3604
3605fn map_open_sqlite_error(err: rusqlite::Error, stage: OpenStage) -> EngineOpenError {
3606 let Some(sqlite_error) = err.sqlite_error() else {
3607 return EngineOpenError::Io { message: "could not open database".to_string() };
3608 };
3609 match sqlite_error.extended_code {
3610 rusqlite::ffi::SQLITE_CORRUPT | rusqlite::ffi::SQLITE_NOTADB => {
3611 EngineOpenError::Corruption(CorruptionDetail {
3612 kind: match stage {
3613 OpenStage::WalReplay => CorruptionKind::WalReplayFailure,
3614 OpenStage::HeaderProbe => CorruptionKind::HeaderMalformed,
3615 OpenStage::SchemaProbe => CorruptionKind::SchemaInconsistent,
3616 OpenStage::EmbedderIdentity => CorruptionKind::EmbedderIdentityDrift,
3617 },
3618 stage,
3619 locator: CorruptionLocator::OpaqueSqliteError {
3620 sqlite_extended_code: sqlite_error.extended_code,
3621 },
3622 recovery_hint: RecoveryHint {
3623 code: match stage {
3624 OpenStage::WalReplay => "E_CORRUPT_WAL_REPLAY",
3625 OpenStage::HeaderProbe => "E_CORRUPT_HEADER",
3626 OpenStage::SchemaProbe => "E_CORRUPT_SCHEMA",
3627 OpenStage::EmbedderIdentity => "E_CORRUPT_EMBEDDER_IDENTITY",
3628 },
3629 doc_anchor: match stage {
3630 OpenStage::WalReplay => "design/recovery.md#wal-replay-failures",
3631 OpenStage::HeaderProbe => "design/recovery.md#header-malformed",
3632 OpenStage::SchemaProbe => "design/recovery.md#schema-inconsistent",
3633 OpenStage::EmbedderIdentity => "design/recovery.md#embedder-identity-drift",
3634 },
3635 },
3636 })
3637 }
3638 _ => EngineOpenError::Io { message: "could not open database".to_string() },
3639 }
3640}
3641
3642fn emit_open_error_event(subscriber: &Arc<dyn lifecycle::Subscriber>, err: &EngineOpenError) {
3643 if let EngineOpenError::Corruption(detail) = err {
3644 let code = match detail.locator {
3645 CorruptionLocator::OpaqueSqliteError { sqlite_extended_code } => {
3646 Some(sqlite_extended_code_name_from_int(sqlite_extended_code))
3647 }
3648 _ => None,
3649 };
3650 let event = lifecycle::Event {
3651 phase: lifecycle::Phase::Failed,
3652 source: lifecycle::EventSource::SqliteInternal,
3653 category: lifecycle::EventCategory::Corruption,
3654 code,
3655 };
3656 subscriber.on_event(&event);
3657 }
3658}
3659
3660#[allow(clippy::vec_box)]
3675fn install_profile_callback(
3676 connection: &Connection,
3677 subscribers: &Arc<lifecycle::SubscriberRegistry>,
3678 profiling_enabled: &Arc<AtomicBool>,
3679 slow_threshold_ms: &Arc<AtomicU64>,
3680 contexts: &mut Vec<Box<ProfileContext>>,
3681) {
3682 let mut ctx = Box::new(ProfileContext {
3683 subscribers: Arc::clone(subscribers),
3684 profiling_enabled: Arc::clone(profiling_enabled),
3685 slow_threshold_ms: Arc::clone(slow_threshold_ms),
3686 });
3687 let ctx_ptr: *mut ProfileContext = &mut *ctx;
3688
3689 unsafe {
3701 rusqlite::ffi::sqlite3_profile(
3702 connection.handle(),
3703 Some(profile_callback_trampoline),
3704 ctx_ptr.cast::<std::ffi::c_void>(),
3705 );
3706 }
3707 contexts.push(ctx);
3708}
3709
3710fn uninstall_profile_callback(connection: &Connection) {
3714 unsafe {
3717 rusqlite::ffi::sqlite3_profile(connection.handle(), None, std::ptr::null_mut());
3718 }
3719}
3720
3721fn configure_reader_lookaside(connection: &Connection) -> std::os::raw::c_int {
3738 unsafe {
3748 rusqlite::ffi::sqlite3_db_config(
3749 connection.handle(),
3750 rusqlite::ffi::SQLITE_DBCONFIG_LOOKASIDE,
3751 std::ptr::null_mut::<std::ffi::c_void>(),
3752 READER_LOOKASIDE_SLOT_SIZE,
3753 READER_LOOKASIDE_SLOT_COUNT,
3754 )
3755 }
3756}
3757
3758#[cfg(debug_assertions)]
3766fn read_lookaside_used_hiwtr(connection: &Connection) -> std::os::raw::c_int {
3767 let mut current: std::os::raw::c_int = 0;
3768 let mut hiwtr: std::os::raw::c_int = 0;
3769 unsafe {
3772 rusqlite::ffi::sqlite3_db_status(
3773 connection.handle(),
3774 rusqlite::ffi::SQLITE_DBSTATUS_LOOKASIDE_USED,
3775 &mut current,
3776 &mut hiwtr,
3777 0,
3778 );
3779 }
3780 hiwtr
3781}
3782
3783#[cfg(debug_assertions)]
3790fn read_cache_status(
3791 connection: &Connection,
3792) -> (std::os::raw::c_int, std::os::raw::c_int, std::os::raw::c_int) {
3793 let mut hit_current: std::os::raw::c_int = 0;
3794 let mut hit_hiwtr: std::os::raw::c_int = 0;
3795 let mut miss_current: std::os::raw::c_int = 0;
3796 let mut miss_hiwtr: std::os::raw::c_int = 0;
3797 let mut used_current: std::os::raw::c_int = 0;
3798 let mut used_hiwtr: std::os::raw::c_int = 0;
3799 unsafe {
3803 rusqlite::ffi::sqlite3_db_status(
3804 connection.handle(),
3805 rusqlite::ffi::SQLITE_DBSTATUS_CACHE_HIT,
3806 &mut hit_current,
3807 &mut hit_hiwtr,
3808 0,
3809 );
3810 rusqlite::ffi::sqlite3_db_status(
3811 connection.handle(),
3812 rusqlite::ffi::SQLITE_DBSTATUS_CACHE_MISS,
3813 &mut miss_current,
3814 &mut miss_hiwtr,
3815 0,
3816 );
3817 rusqlite::ffi::sqlite3_db_status(
3818 connection.handle(),
3819 rusqlite::ffi::SQLITE_DBSTATUS_CACHE_USED,
3820 &mut used_current,
3821 &mut used_hiwtr,
3822 0,
3823 );
3824 }
3825 (hit_current, miss_current, used_current)
3829}
3830
3831unsafe extern "C" fn profile_callback_trampoline(
3845 user_data: *mut std::ffi::c_void,
3846 sql: *const std::os::raw::c_char,
3847 nanoseconds: u64,
3848) {
3849 if user_data.is_null() || sql.is_null() {
3850 return;
3851 }
3852 let ctx = unsafe { &*(user_data.cast::<ProfileContext>()) };
3853 let sql_text = match unsafe { std::ffi::CStr::from_ptr(sql) }.to_str() {
3854 Ok(s) => s,
3855 Err(_) => return,
3856 };
3857
3858 let wall_clock_ms = nanoseconds / 1_000_000;
3859
3860 if ctx.profiling_enabled.load(Ordering::Relaxed) {
3861 let record = lifecycle::ProfileRecord {
3862 wall_clock_ms,
3863 step_count: 0,
3869 cache_delta: 0,
3870 };
3871 ctx.subscribers.dispatch_profile(&record);
3872 }
3873
3874 let threshold = ctx.slow_threshold_ms.load(Ordering::Relaxed);
3875 if wall_clock_ms > threshold {
3876 let signal = lifecycle::SlowStatement { statement: sql_text.to_string(), wall_clock_ms };
3877 ctx.subscribers.dispatch_slow_statement(&signal);
3878 }
3879}
3880
3881#[cfg(test)]
3882mod tests {
3883 use super::{Engine, PreparedWrite};
3884 use tempfile::TempDir;
3885
3886 #[test]
3887 fn write_advances_cursor() {
3888 let dir = TempDir::new().unwrap();
3889 let opened = Engine::open(dir.path().join("rewrite.sqlite")).expect("engine should open");
3890 let receipt = opened
3891 .engine
3892 .write(&[PreparedWrite::Node {
3893 kind: "doc".to_string(),
3894 body: "hello".to_string(),
3895 source_id: None,
3896 }])
3897 .expect("write should succeed");
3898
3899 assert_eq!(receipt.cursor, 1);
3900 }
3901}