1use std::path::Path;
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
10use std::sync::Arc;
11
12use rusqlite::OptionalExtension;
13
14use crate::database_owner_identity::{DatabaseOwnerIdentity, DatabaseOwnerIdentityError};
15use crate::error::SqliteError;
16use crate::pool::{ConnectionPool, PoolConfig, WalCeilingPolicy};
17use crate::stores::{agents, attachment, blob, entity, event, graph, note, sparse, text, vectors};
18
19mod capability_factories;
20mod code_map;
21mod core_stores;
22#[path = "backend/schema_readiness.rs"]
23mod memory_visibility;
24mod pack_schema;
25mod policy_open;
26
27#[cfg(test)]
28#[path = "backend/filter_builder_tests.rs"]
29mod filter_builder_tests;
30
31mod namespace_listing;
32pub use namespace_listing::NamespaceLiveness;
33
34#[cfg(test)]
35#[path = "backend/memory_visibility_tests.rs"]
36mod memory_visibility_tests;
37
38#[cfg(any(unix, windows))]
39mod claimed_file_identity;
40
41fn sqlite_table_exists(conn: &rusqlite::Connection, table: &str) -> Result<bool, SqliteError> {
42 conn.query_row(
43 "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1",
44 rusqlite::params![table],
45 |row| row.get::<_, i64>(0),
46 )
47 .optional()
48 .map(|row| row.is_some())
49 .map_err(SqliteError::Rusqlite)
50}
51
52fn ensure_fts_rowid_map_backfilled(
112 conn: &rusqlite::Connection,
113 table: &str,
114 admission: &crate::pool::WriteAdmission,
115) -> Result<bool, SqliteError> {
116 let map = text::rowid_map_table(table);
117 let state = text::rowid_map_state_table(table);
118 let already_backfilled: bool = conn.query_row(
119 &format!("SELECT EXISTS(SELECT 1 FROM {state} WHERE key = 'backfill' AND value = ?1)"),
120 rusqlite::params![text::ROWID_MAP_BACKFILL_COMPLETE],
121 |row| row.get(0),
122 )?;
123 if already_backfilled {
124 return Ok(true);
125 }
126 let fts_has_a_row: bool = conn.query_row(
127 &format!("SELECT EXISTS(SELECT 1 FROM {table} LIMIT 1)"),
128 [],
129 |row| row.get(0),
130 )?;
131 if !fts_has_a_row {
132 return Ok(false);
133 }
134
135 conn.execute_batch("BEGIN IMMEDIATE")?;
136 if let Err(error) = admission.check() {
137 return Err(crate::migrations::capacity_refusal_after_rollback(
138 conn,
139 conn.execute_batch("ROLLBACK"),
140 error,
141 "FTS rowid-map backfill",
142 ));
143 }
144 let result: Result<(), SqliteError> = (|| {
145 conn.execute_batch(&format!(
146 "DELETE FROM {map} WHERE NOT EXISTS ( \
147 SELECT 1 FROM {table} \
148 WHERE {table}.rowid = {map}.rowid \
149 AND {table}.namespace = {map}.namespace \
150 AND {table}.subject_id = {map}.subject_id \
151 )"
152 ))?;
153 conn.execute_batch(&format!(
154 "INSERT OR REPLACE INTO {map} (namespace, subject_id, rowid) \
155 SELECT namespace, subject_id, rowid FROM {table} \
156 WHERE namespace IS NOT NULL AND subject_id IS NOT NULL \
157 ORDER BY updated_at ASC, rowid ASC"
158 ))?;
159 conn.execute_batch(&format!(
160 "DELETE FROM {table} \
161 WHERE namespace IS NOT NULL AND subject_id IS NOT NULL \
162 AND rowid NOT IN (SELECT rowid FROM {map})"
163 ))?;
164 conn.execute(
165 &format!("INSERT OR REPLACE INTO {state} (key, value) VALUES ('backfill', ?1)"),
166 rusqlite::params![text::ROWID_MAP_BACKFILL_COMPLETE],
167 )?;
168 Ok(())
169 })();
170
171 match result {
172 Ok(()) => {
173 conn.execute_batch("COMMIT")?;
174 Ok(true)
175 }
176 Err(e) => {
177 let _ = conn.execute_batch("ROLLBACK");
178 Err(e)
179 }
180 }
181}
182
183fn warn_scan_fallback_once(table: &str) {
189 static WARNED: std::sync::Once = std::sync::Once::new();
190 WARNED.call_once(|| {
191 tracing::warn!(
192 table,
193 "opened a read-only text-search table with no rowid-map sidecar, or with a sidecar \
194 that has never proven a completed backfill (no durable completion marker); a \
195 read-only connection cannot create, backfill, or reconcile the map itself, so this \
196 falls back to pre-map namespace/subject_id scan predicates for get/delete on this \
197 table rather than trusting a map that might be partial"
198 );
199 });
200}
201
202fn validate_vector_model_key(model_key: &str) -> Result<(), SqliteError> {
203 if model_key.is_empty()
204 || !model_key
205 .chars()
206 .all(|c| c.is_ascii_alphanumeric() || c == '_')
207 {
208 return Err(SqliteError::InvalidData(format!(
209 "invalid model_key '{}': must be non-empty and contain only \
210 alphanumeric/underscore characters",
211 model_key
212 )));
213 }
214 Ok(())
215}
216
217fn validate_vector_table_columns(
218 conn: &rusqlite::Connection,
219 table: &str,
220) -> Result<(), SqliteError> {
221 let pragma = format!("PRAGMA table_xinfo({table})");
222 let mut stmt = conn.prepare(&pragma)?;
223 let mut rows = stmt.query([])?;
224 let mut has_field = false;
225 let mut has_embedding_model = false;
226 while let Some(row) = rows.next()? {
227 let name: String = row.get(1)?;
228 if name == "field" {
229 has_field = true;
230 }
231 if name == "embedding_model" {
232 has_embedding_model = true;
233 }
234 }
235 if !has_field || !has_embedding_model {
236 return Err(SqliteError::InvalidData(format!(
237 "vec0 table '{table}' is missing required column(s) (field={has_field}, \
238 embedding_model={has_embedding_model}); this is a pre-v0.2.8 vector schema and is \
239 not supported — recreate the database"
240 )));
241 }
242 Ok(())
243}
244
245#[derive(Clone, Copy, Debug)]
246pub(crate) enum StoreSchemaKind {
247 Entities,
248 Graph,
249 Notes,
250 Events,
251 Agents,
252}
253
254impl StoreSchemaKind {
255 fn initializer(self) -> fn(&rusqlite::Connection) -> Result<(), rusqlite::Error> {
256 match self {
257 Self::Entities => entity::ensure_entities_schema,
258 Self::Graph => graph::ensure_graph_schema,
259 Self::Notes => note::ensure_notes_schema,
260 Self::Events => event::ensure_events_schema,
261 Self::Agents => agents::ensure_agents_schema,
262 }
263 }
264}
265
266#[derive(Default)]
267pub(crate) struct StoreSchemaGate {
268 pub(crate) ready: AtomicBool,
269 #[cfg(test)]
270 pub(crate) attempts: AtomicUsize,
271}
272
273impl StoreSchemaGate {
274 pub(crate) fn ensure(
275 &self,
276 conn: &rusqlite::Connection,
277 ensure: fn(&rusqlite::Connection) -> Result<(), rusqlite::Error>,
278 ) -> Result<(), rusqlite::Error> {
279 if self.ready.load(Ordering::Acquire) {
282 return Ok(());
283 }
284 #[cfg(test)]
285 self.attempts.fetch_add(1, Ordering::Relaxed);
286 ensure(conn)?;
287 self.ready.store(true, Ordering::Release);
288 Ok(())
289 }
290}
291
292pub struct StorageBackend {
298 pool: Arc<ConnectionPool>,
299 is_file_backed: bool,
300 path: Option<std::path::PathBuf>,
301 vector_tables_ready: parking_lot::Mutex<std::collections::HashSet<String>>,
309 text_tables_ready: parking_lot::Mutex<std::collections::HashSet<String>>,
317 notes_seq_repair_runs: AtomicUsize,
324 store_schemas: [Arc<StoreSchemaGate>; 5],
325}
326
327impl StorageBackend {
328 pub fn sqlite(path: impl AsRef<Path>) -> Result<Self, SqliteError> {
342 Self::sqlite_with_pool_config(path, PoolConfig::default(), None)
343 }
344
345 #[cfg(any(test, feature = "test-support"))]
347 pub fn sqlite_for_test(path: impl AsRef<Path>) -> Result<Self, SqliteError> {
348 Self::sqlite_with_pool_config(path, PoolConfig::for_test(), None)
349 }
350
351 #[cfg(any(test, feature = "test-support"))]
353 pub fn sqlite_for_test_with_journal_mode(
354 path: impl AsRef<Path>,
355 wal_mode: bool,
356 busy_timeout: std::time::Duration,
357 ) -> Result<Self, SqliteError> {
358 Self::sqlite_with_pool_config(
359 path,
360 PoolConfig {
361 wal_mode,
362 busy_timeout,
363 write_queue_enabled: Some(true),
364 ..PoolConfig::for_test()
365 },
366 None,
367 )
368 }
369
370 #[cfg(any(test, feature = "test-support"))]
375 pub fn sqlite_for_test_with_journal_mode_in(
376 path: impl AsRef<Path>,
377 wal_mode: bool,
378 busy_timeout: std::time::Duration,
379 volume_lock_dir: std::path::PathBuf,
380 ) -> Result<Self, SqliteError> {
381 Self::sqlite_with_pool_config(
382 path,
383 PoolConfig {
384 wal_mode,
385 busy_timeout,
386 write_queue_enabled: Some(true),
387 volume_lock_dir: Some(volume_lock_dir),
388 ..PoolConfig::for_test()
389 },
390 None,
391 )
392 }
393
394 pub fn sqlite_with_max_readers(
398 path: impl AsRef<Path>,
399 max_readers: Option<usize>,
400 ) -> Result<Self, SqliteError> {
401 Self::sqlite_with_pool_config(path, PoolConfig::default(), max_readers)
402 }
403
404 pub fn sqlite_with_max_readers_and_wal_ceiling(
408 path: impl AsRef<Path>,
409 max_readers: Option<usize>,
410 wal_ceiling: WalCeilingPolicy,
411 ) -> Result<Self, SqliteError> {
412 Self::sqlite_with_pool_config(
413 path,
414 PoolConfig {
415 wal_ceiling,
416 ..PoolConfig::default()
417 },
418 max_readers,
419 )
420 }
421
422 pub(crate) fn sqlite_with_pool_config(
423 path: impl AsRef<Path>,
424 pool_config: PoolConfig,
425 max_readers: Option<usize>,
426 ) -> Result<Self, SqliteError> {
427 crate::extension::ensure_extensions_loaded();
428 let resolved = path.as_ref().to_path_buf();
429 let read_only =
430 std::fs::metadata(&resolved).is_ok_and(|metadata| metadata.permissions().readonly());
431 let mut config = PoolConfig {
432 path: Some(resolved.clone()),
433 read_only,
434 ..pool_config
435 };
436 if let Some(max_readers) = max_readers {
437 config.max_readers = max_readers;
438 }
439 if read_only {
440 config.write_queue_enabled = Some(false);
441 }
442 let pool = ConnectionPool::new(config)?;
443 Ok(Self {
444 pool: Arc::new(pool),
445 is_file_backed: true,
446 path: Some(resolved),
447 vector_tables_ready: Default::default(),
448 text_tables_ready: Default::default(),
449 notes_seq_repair_runs: AtomicUsize::new(0),
450 store_schemas: std::array::from_fn(|_| Arc::new(StoreSchemaGate::default())),
451 })
452 }
453
454 pub fn sqlite_read_only(path: impl AsRef<Path>) -> Result<Self, SqliteError> {
465 Self::sqlite_read_only_with_pool_config(path, PoolConfig::default(), None)
466 }
467
468 #[cfg(any(test, feature = "test-support"))]
470 pub fn sqlite_read_only_for_test(path: impl AsRef<Path>) -> Result<Self, SqliteError> {
471 Self::sqlite_read_only_with_pool_config(path, PoolConfig::for_test(), None)
472 }
473
474 pub fn sqlite_read_only_with_max_readers(
476 path: impl AsRef<Path>,
477 max_readers: Option<usize>,
478 ) -> Result<Self, SqliteError> {
479 Self::sqlite_read_only_with_pool_config(path, PoolConfig::default(), max_readers)
480 }
481
482 pub fn sqlite_read_only_with_max_readers_and_wal_ceiling(
485 path: impl AsRef<Path>,
486 max_readers: Option<usize>,
487 wal_ceiling: WalCeilingPolicy,
488 ) -> Result<Self, SqliteError> {
489 Self::sqlite_read_only_with_pool_config(
490 path,
491 PoolConfig {
492 wal_ceiling,
493 ..PoolConfig::default()
494 },
495 max_readers,
496 )
497 }
498
499 fn sqlite_read_only_with_pool_config(
500 path: impl AsRef<Path>,
501 pool_config: PoolConfig,
502 max_readers: Option<usize>,
503 ) -> Result<Self, SqliteError> {
504 crate::extension::ensure_extensions_loaded();
505 let resolved = path.as_ref().to_path_buf();
506 let mut config = PoolConfig {
507 path: Some(resolved.clone()),
508 read_only: true,
509 write_queue_enabled: Some(false),
510 ..pool_config
511 };
512 if let Some(max_readers) = max_readers {
513 config.max_readers = max_readers;
514 }
515 let pool = ConnectionPool::new(config)?;
521 Ok(Self {
522 pool: Arc::new(pool),
523 is_file_backed: true,
524 path: Some(resolved),
525 vector_tables_ready: Default::default(),
526 text_tables_ready: Default::default(),
527 notes_seq_repair_runs: AtomicUsize::new(0),
528 store_schemas: std::array::from_fn(|_| Arc::new(StoreSchemaGate::default())),
529 })
530 }
531
532 pub fn memory() -> Result<Self, SqliteError> {
538 crate::extension::ensure_extensions_loaded();
539 let config = PoolConfig {
540 path: None,
541 ..PoolConfig::default()
542 };
543 let pool = ConnectionPool::new(config)?;
544 Ok(Self {
545 pool: Arc::new(pool),
546 is_file_backed: false,
547 path: None,
548 vector_tables_ready: Default::default(),
549 text_tables_ready: Default::default(),
550 notes_seq_repair_runs: AtomicUsize::new(0),
551 store_schemas: std::array::from_fn(|_| Arc::new(StoreSchemaGate::default())),
552 })
553 }
554
555 pub fn apply_schema(
562 &self,
563 plan: &crate::migrations::ServiceSchemaPlan,
564 ) -> Result<(), SqliteError> {
565 let admission = self.pool.write_admission();
566 crate::migrations::apply_schema_plan_with_admission(
567 &mut self.pool.migration_transactions(),
568 plan,
569 &admission,
570 )
571 }
572
573 pub fn apply_pack_ddl_statements(
589 &self,
590 statements: &[&'static str],
591 ) -> Result<(), SqliteError> {
592 self.apply_pack_ddl_statements_with_columns(statements, &[])
593 }
594
595 pub fn apply_pack_ddl_statements_with_columns(
602 &self,
603 statements: &[&'static str],
604 additions: &[khive_types::PackColumnAddition],
605 ) -> Result<(), SqliteError> {
606 let writer = self.pool.writer_for_admitted_operation()?;
607 writer.transaction(|conn| {
608 pack_schema::add_missing_columns(conn, additions)?;
609 for &stmt in statements {
610 conn.execute_batch(stmt)?;
611 }
612 pack_schema::validate_columns(conn, additions)?;
613 Ok(())
614 })
615 }
616
617 pub fn validate_pack_schema_columns(
620 &self,
621 additions: &[khive_types::PackColumnAddition],
622 ) -> Result<(), SqliteError> {
623 if additions.is_empty() {
624 return Ok(());
625 }
626 let reader = self.pool.reader()?;
627 pack_schema::validate_columns(reader.conn(), additions)
628 }
629
630 pub fn prepare_core_schema(&self) -> Result<u32, SqliteError> {
640 if self.is_read_only() {
641 let reader = self.pool.reader()?;
642 crate::migrations::validate_schema_is_current(reader.conn())
643 } else {
644 let latest = crate::migrations::MIGRATIONS
645 .last()
646 .map(|migration| migration.version)
647 .unwrap_or(0);
648 {
649 let reader = self.pool.reader()?;
650 let current = crate::migrations::read_schema_version(reader.conn())?;
651 if current >= latest {
652 return crate::migrations::validate_schema_is_current(reader.conn());
653 }
654 }
655 let owner = crate::stores::blob::acquire_database_gc_owner_for_path_blocking(
656 self.pool.canonical_path().map(Path::to_path_buf),
657 )
658 .map_err(|error| {
659 SqliteError::InvalidData(format!(
660 "failed to acquire database GC owner before schema preparation: {error}"
661 ))
662 })?;
663 self.run_core_migrations(&owner)
664 }
665 }
666
667 pub fn attachment_cutover_status(
669 &self,
670 ) -> Result<crate::migrations::AttachmentCutoverStatus, SqliteError> {
671 let reader = self.pool.reader()?;
672 crate::migrations::attachment_cutover_status(reader.conn())
673 }
674
675 fn require_attachment_cutover_owner(
676 &self,
677 owner: &crate::stores::blob::DatabaseGcOwnerGuard,
678 ) -> Result<(), SqliteError> {
679 let sql = self.sql();
680 let backend_path = sql.database_path();
681 if owner.database_path() != backend_path.as_deref() {
682 return Err(SqliteError::InvalidData(format!(
683 "attachment cutover GC owner targets {:?}, but this backend is {:?}",
684 owner.database_path(),
685 backend_path.as_deref()
686 )));
687 }
688 Ok(())
689 }
690
691 pub fn stage_attachment_cutover(
695 &self,
696 owner: &crate::stores::blob::DatabaseGcOwnerGuard,
697 ) -> Result<(), SqliteError> {
698 self.require_attachment_cutover_owner(owner)?;
699 if self.is_read_only() {
700 return Err(SqliteError::InvalidData(
701 "cannot stage attachment cutover on a read-only backend".into(),
702 ));
703 }
704 let mut writer = self.pool.writer_for_admitted_operation()?;
705 let admission = self.pool.write_admission();
706 crate::migrations::stage_attachment_cutover_with_admission(writer.conn_mut(), &admission)
707 }
708
709 pub fn apply_verified_attachments(
711 &self,
712 owner: &crate::stores::blob::DatabaseGcOwnerGuard,
713 attachments: &[khive_storage::Attachment],
714 ) -> Result<(), SqliteError> {
715 self.require_attachment_cutover_owner(owner)?;
716 if self.is_read_only() {
717 return Err(SqliteError::InvalidData(
718 "cannot apply verified attachments on a read-only backend".into(),
719 ));
720 }
721 let mut writer = self.pool.writer_for_admitted_operation()?;
722 let tx = writer
723 .conn_mut()
724 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
725 let admission = self.pool.write_admission();
726 if let Err(error) = admission.check() {
727 let rollback = tx.rollback();
728 return Err(crate::migrations::capacity_refusal_after_rollback(
729 writer.conn(),
730 rollback,
731 error,
732 "verified attachment publication",
733 ));
734 }
735 for attachment in attachments {
736 attachment
737 .validate()
738 .map_err(|error| SqliteError::InvalidData(error.to_string()))?;
739 crate::migrations::apply_generic_verified_attachment(
740 &tx,
741 &attachment.record_uuid.to_string(),
742 attachment.substrate.as_str(),
743 &attachment.role,
744 &attachment.content_ref,
745 attachment.media_type.as_deref(),
746 attachment.size_bytes,
747 attachment.created_at,
748 )?;
749 }
750 tx.commit()?;
751 Ok(())
752 }
753
754 pub fn finalize_attachment_cutover(
757 &self,
758 owner: &crate::stores::blob::DatabaseGcOwnerGuard,
759 ) -> Result<(), SqliteError> {
760 self.require_attachment_cutover_owner(owner)?;
761 if self.is_read_only() {
762 return Err(SqliteError::InvalidData(
763 "cannot finalize attachment cutover on a read-only backend".into(),
764 ));
765 }
766 let mut writer = self.pool.writer_for_admitted_operation()?;
767 let admission = self.pool.write_admission();
768 crate::migrations::finalize_attachment_cutover_with_admission(writer.conn_mut(), &admission)
769 }
770
771 pub fn entities(&self) -> Result<Arc<dyn khive_storage::EntityStore>, SqliteError> {
775 self.entities_for_namespace("local")
776 }
777
778 pub fn entities_for_namespace(
782 &self,
783 namespace: &str,
784 ) -> Result<Arc<dyn khive_storage::EntityStore>, SqliteError> {
785 if namespace.trim().is_empty() {
786 return Err(SqliteError::InvalidData(
787 "entities namespace must be non-empty".to_string(),
788 ));
789 }
790 self.ensure_store_schema(StoreSchemaKind::Entities)?;
791
792 Ok(Arc::new(entity::SqlEntityStore::new(
793 Arc::clone(&self.pool),
794 self.is_file_backed,
795 )))
796 }
797
798 pub fn attachments(&self) -> Result<Arc<dyn khive_storage::AttachmentStore>, SqliteError> {
805 Ok(Arc::new(attachment::SqlAttachmentStore::new(
806 Arc::clone(&self.pool),
807 self.is_file_backed,
808 )))
809 }
810
811 pub fn graph(&self) -> Result<Arc<dyn khive_storage::GraphStore>, SqliteError> {
816 self.graph_for_namespace("local")
817 }
818
819 pub fn graph_for_namespace(
821 &self,
822 namespace: &str,
823 ) -> Result<Arc<dyn khive_storage::GraphStore>, SqliteError> {
824 if namespace.trim().is_empty() {
825 return Err(SqliteError::InvalidData(
826 "graph namespace must be non-empty".to_string(),
827 ));
828 }
829 self.ensure_store_schema(StoreSchemaKind::Graph)?;
830
831 Ok(Arc::new(
832 graph::SqlGraphStore::new_scoped(
833 Arc::clone(&self.pool),
834 self.is_file_backed,
835 namespace.trim().to_string(),
836 )
837 .with_index_repair(crate::stores::index_repair::IndexRepairContext::new(
838 Arc::clone(&self.pool),
839 self.store_schemas.clone(),
840 crate::stores::index_repair::IndexReadKind::Graph,
841 )),
842 ))
843 }
844
845 fn ensure_store_schema(&self, kind: StoreSchemaKind) -> Result<(), SqliteError> {
846 if self.is_read_only()
847 || self.store_schemas[kind as usize]
848 .ready
849 .load(Ordering::Acquire)
850 {
851 return Ok(());
852 }
853 let writer = self.constructor_writer()?;
854 self.ensure_store_schema_with_writer(kind, writer.conn())
855 }
856
857 fn ensure_store_schema_with_writer(
858 &self,
859 kind: StoreSchemaKind,
860 conn: &rusqlite::Connection,
861 ) -> Result<(), SqliteError> {
862 self.store_schemas[kind as usize].ensure(conn, kind.initializer())?;
863 Ok(())
864 }
865
866 fn constructor_writer(
867 &self,
868 ) -> Result<crate::pool::PooledAutocommitWriteUnit<'_>, SqliteError> {
869 let context = khive_storage::capture_request_read_context();
870 let Some(operation) = context.store_acquisition_operation() else {
871 return self.pool.autocommit_write_unit();
872 };
873 let writer = self
874 .pool
875 .writer_until_for_admitted_operation(|| context.blocking_stop_reason().is_some())?
876 .ok_or_else(|| {
877 SqliteError::RequestReadStopped(khive_storage::StorageError::Timeout {
878 operation: operation.into(),
879 })
880 })?;
881 writer.admit_autocommit()
882 }
883
884 pub fn notes(&self) -> Result<Arc<dyn khive_storage::NoteStore>, SqliteError> {
888 self.notes_for_namespace("local")
889 }
890
891 pub fn notes_for_namespace(
895 &self,
896 namespace: &str,
897 ) -> Result<Arc<dyn khive_storage::NoteStore>, SqliteError> {
898 if namespace.trim().is_empty() {
899 return Err(SqliteError::InvalidData(
900 "notes namespace must be non-empty".to_string(),
901 ));
902 }
903 if !self.is_read_only()
904 && (!self.store_schemas[StoreSchemaKind::Notes as usize]
905 .ready
906 .load(Ordering::Acquire)
907 || self.notes_seq_repair_runs.load(Ordering::Relaxed) == 0)
908 {
909 let writer = self.constructor_writer()?;
910 self.ensure_store_schema_with_writer(StoreSchemaKind::Notes, writer.conn())?;
911
912 if self.notes_seq_repair_runs.load(Ordering::Relaxed) == 0 {
919 note::repair_notes_seq(writer.conn())?;
920 self.notes_seq_repair_runs.fetch_add(1, Ordering::Relaxed);
921 }
922 }
923
924 Ok(Arc::new(
925 note::SqlNoteStore::new(Arc::clone(&self.pool), self.is_file_backed).with_index_repair(
926 crate::stores::index_repair::IndexRepairContext::new(
927 Arc::clone(&self.pool),
928 self.store_schemas.clone(),
929 crate::stores::index_repair::IndexReadKind::Notes,
930 ),
931 ),
932 ))
933 }
934
935 pub fn notes_seq_repair_run_count(&self) -> usize {
940 self.notes_seq_repair_runs.load(Ordering::Relaxed)
941 }
942
943 pub fn events(&self) -> Result<Arc<dyn khive_storage::EventStore>, SqliteError> {
948 self.events_for_namespace("local")
949 }
950
951 pub fn events_for_namespace(
953 &self,
954 namespace: &str,
955 ) -> Result<Arc<dyn khive_storage::EventStore>, SqliteError> {
956 if namespace.trim().is_empty() {
957 return Err(SqliteError::InvalidData(
958 "events namespace must be non-empty".to_string(),
959 ));
960 }
961 self.ensure_store_schema(StoreSchemaKind::Events)?;
962
963 Ok(Arc::new(event::SqlEventStore::new_scoped(
964 Arc::clone(&self.pool),
965 self.is_file_backed,
966 namespace.trim().to_string(),
967 )))
968 }
969
970 pub fn agents(&self) -> Result<Arc<dyn khive_storage::AgentStore>, SqliteError> {
975 self.ensure_store_schema(StoreSchemaKind::Agents)?;
976
977 Ok(Arc::new(agents::SqlAgentStore::new(
978 Arc::clone(&self.pool),
979 self.is_file_backed,
980 )))
981 }
982
983 pub fn vectors(
989 &self,
990 model_key: &str,
991 embedding_model: &str,
992 dimensions: usize,
993 ) -> Result<Arc<dyn khive_storage::VectorStore>, SqliteError> {
994 self.vectors_for_namespace(model_key, embedding_model, dimensions, "local")
995 }
996
997 pub fn vectors_for_namespace(
1007 &self,
1008 model_key: &str,
1009 embedding_model: &str,
1010 dimensions: usize,
1011 namespace: &str,
1012 ) -> Result<Arc<dyn khive_storage::VectorStore>, SqliteError> {
1013 validate_vector_model_key(model_key)?;
1014 if namespace.trim().is_empty() {
1015 return Err(SqliteError::InvalidData(
1016 "vector store namespace must be non-empty".to_string(),
1017 ));
1018 }
1019 self.ensure_vector_tables(&[(model_key, dimensions)])?;
1020 Ok(Arc::new(vectors::SqliteVecStore::new(
1021 Arc::clone(&self.pool),
1022 self.is_file_backed,
1023 model_key.to_string(),
1024 embedding_model.to_string(),
1025 dimensions,
1026 namespace.trim().to_string(),
1027 )?))
1028 }
1029
1030 pub fn ensure_vector_tables(&self, models: &[(&str, usize)]) -> Result<(), SqliteError> {
1039 for (model_key, _) in models {
1040 validate_vector_model_key(model_key)?;
1041 }
1042 if models.is_empty() {
1043 return Ok(());
1044 }
1045
1046 crate::extension::ensure_extensions_loaded();
1048
1049 if self.is_read_only() {
1054 let reader = self.pool.reader()?;
1058 for (model_key, _) in models {
1059 let table = format!("vec_{model_key}");
1060 if !sqlite_table_exists(reader.conn(), &table)? {
1061 return Err(SqliteError::InvalidData(format!(
1062 "read-only database has no vector table '{table}'; create and populate it in \
1063 a writable copy before opening the snapshot"
1064 )));
1065 }
1066 validate_vector_table_columns(reader.conn(), &table)?;
1067 }
1068 return Ok(());
1069 }
1070
1071 let pending = self.unprepared_vector_tables(models);
1072 if pending.is_empty() {
1073 return Ok(());
1074 }
1075 let writer = self.constructor_writer()?;
1076 let pending = self.unprepared_vector_tables(&pending);
1079 if pending.is_empty() {
1080 return Ok(());
1081 }
1082
1083 for (model_key, _) in &pending {
1087 let table = format!("vec_{model_key}");
1088 if sqlite_table_exists(writer.conn(), &table)? {
1094 validate_vector_table_columns(writer.conn(), &table)?;
1095 }
1096 }
1097
1098 writer
1107 .conn()
1108 .execute_batch(crate::migrations::EMBEDDING_MODELS_DDL)?;
1109
1110 writer
1114 .conn()
1115 .execute_batch(crate::migrations::ANN_WRITE_LOG_DDL)?;
1116 writer
1117 .conn()
1118 .execute_batch(crate::migrations::ANN_WRITE_LOG_MODEL_SEQ_INDEX_DDL)?;
1119 writer
1120 .conn()
1121 .execute_batch(crate::migrations::ANN_CONSUMER_PENDING_DDL)?;
1122 for (model_key, dimensions) in &pending {
1124 let ddl = format!(
1125 "CREATE VIRTUAL TABLE IF NOT EXISTS vec_{} USING vec0(\
1126 subject_id TEXT PRIMARY KEY, \
1127 namespace TEXT NOT NULL, \
1128 kind TEXT NOT NULL, \
1129 field TEXT NOT NULL, \
1130 embedding_model TEXT NOT NULL, \
1131 embedding float[{}] distance_metric=cosine\
1132 )",
1133 model_key, dimensions
1134 );
1135 writer.conn().execute_batch(&ddl)?;
1136 }
1137 self.vector_tables_ready.lock().extend(
1140 pending
1141 .iter()
1142 .map(|(model_key, _)| (*model_key).to_string()),
1143 );
1144 Ok(())
1145 }
1146
1147 fn unprepared_vector_tables<'a>(&self, models: &[(&'a str, usize)]) -> Vec<(&'a str, usize)> {
1149 let ready = self.vector_tables_ready.lock();
1150 models
1151 .iter()
1152 .filter(|(model_key, _)| !ready.contains(*model_key))
1153 .copied()
1154 .collect()
1155 }
1156
1157 pub fn register_embedding_model(
1162 &self,
1163 engine_name: &str,
1164 model_id: &str,
1165 key_version: &str,
1166 dimensions: u32,
1167 ) -> Result<(), SqliteError> {
1168 let writer = self.pool.autocommit_write_unit()?;
1169 writer
1170 .conn()
1171 .execute_batch(crate::migrations::EMBEDDING_MODELS_DDL)
1172 .map_err(|error| {
1173 SqliteError::write(error, khive_storage::error::SqliteWriteStage::Statement)
1174 })?;
1175
1176 let now = chrono::Utc::now().timestamp_micros();
1177 let canonical_key =
1178 format!("{engine_name}:{model_id}:{key_version}:{dimensions}").into_bytes();
1179 let id = uuid::Uuid::new_v4();
1180 writer
1181 .conn()
1182 .execute(
1183 "INSERT INTO _embedding_models \
1184 (id, engine_name, model_id, key_version, dim, output_dim, status, \
1185 activated_at, superseded_at, superseded_by, canonical_key, created_at) \
1186 VALUES (?1, ?2, ?3, ?4, ?5, NULL, 'active', ?6, NULL, NULL, ?7, ?8) \
1187 ON CONFLICT(canonical_key) DO UPDATE SET \
1188 status = 'active', \
1189 activated_at = COALESCE(_embedding_models.activated_at, excluded.activated_at)",
1190 rusqlite::params![
1191 id.as_bytes().as_slice(),
1192 engine_name,
1193 model_id,
1194 key_version,
1195 dimensions as i64,
1196 now,
1197 canonical_key,
1198 now,
1199 ],
1200 )
1201 .map_err(|error| {
1202 SqliteError::write(error, khive_storage::error::SqliteWriteStage::Statement)
1203 })?;
1204 Ok(())
1205 }
1206
1207 pub fn sparse(
1211 &self,
1212 model_key: &str,
1213 ) -> Result<Arc<dyn khive_storage::SparseStore>, SqliteError> {
1214 self.sparse_for_namespace(model_key, "local")
1215 }
1216
1217 pub fn sparse_for_namespace(
1221 &self,
1222 model_key: &str,
1223 namespace: &str,
1224 ) -> Result<Arc<dyn khive_storage::SparseStore>, SqliteError> {
1225 if model_key.is_empty()
1226 || !model_key
1227 .chars()
1228 .all(|c| c.is_ascii_alphanumeric() || c == '_')
1229 {
1230 return Err(SqliteError::InvalidData(format!(
1231 "invalid model_key '{}': must be non-empty and contain only alphanumeric/underscore characters",
1232 model_key
1233 )));
1234 }
1235 if namespace.trim().is_empty() {
1236 return Err(SqliteError::InvalidData(
1237 "sparse store namespace must be non-empty".to_string(),
1238 ));
1239 }
1240
1241 if self.is_read_only() {
1242 let table = format!("sparse_{model_key}");
1243 let reader = self.pool.reader()?;
1244 if !sqlite_table_exists(reader.conn(), &table)? {
1245 return Err(SqliteError::InvalidData(format!(
1246 "read-only database has no sparse table '{table}'; create and populate it in \
1247 a writable copy before opening the snapshot"
1248 )));
1249 }
1250 } else {
1251 let writer = self.pool.autocommit_write_unit()?;
1252 sparse::ensure_sparse_schema(writer.conn(), model_key)
1253 .map_err(SqliteError::Rusqlite)?;
1254 }
1255
1256 Ok(Arc::new(sparse::SqliteSparseStore::new(
1257 Arc::clone(&self.pool),
1258 self.is_file_backed,
1259 model_key.to_string(),
1260 namespace.trim().to_string(),
1261 )?))
1262 }
1263
1264 pub fn text(&self, table_key: &str) -> Result<Arc<dyn khive_storage::TextSearch>, SqliteError> {
1271 self.text_with_tokenizer(table_key, "trigram")
1272 }
1273
1274 pub fn text_with_tokenizer(
1282 &self,
1283 table_key: &str,
1284 tokenizer: &str,
1285 ) -> Result<Arc<dyn khive_storage::TextSearch>, SqliteError> {
1286 if table_key.is_empty()
1287 || !table_key
1288 .chars()
1289 .all(|c| c.is_ascii_alphanumeric() || c == '_')
1290 {
1291 return Err(SqliteError::InvalidData(format!(
1292 "invalid table_key '{}': must be non-empty and contain only \
1293 alphanumeric/underscore characters",
1294 table_key
1295 )));
1296 }
1297 if table_key.ends_with("_rowids") || table_key.ends_with("_rowids_state") {
1307 return Err(SqliteError::InvalidData(format!(
1308 "invalid table_key '{}': must not end in '_rowids' or '_rowids_state' — those \
1309 suffixes are reserved for a text table's own rowid-map sidecar and its \
1310 completion-marker state table (see text::rowid_map_table, \
1311 text::rowid_map_state_table)",
1312 table_key
1313 )));
1314 }
1315 if tokenizer.is_empty()
1316 || !tokenizer
1317 .chars()
1318 .all(|c| c.is_ascii_alphanumeric() || c == '_')
1319 {
1320 return Err(SqliteError::InvalidData(format!(
1321 "invalid tokenizer '{}': must be non-empty and contain only \
1322 alphanumeric/underscore characters",
1323 tokenizer
1324 )));
1325 }
1326
1327 let ddl = format!(
1328 "CREATE VIRTUAL TABLE IF NOT EXISTS fts_{} USING fts5(\
1329 subject_id UNINDEXED, \
1330 kind UNINDEXED, \
1331 title, \
1332 body, \
1333 tags UNINDEXED, \
1334 namespace UNINDEXED, \
1335 metadata UNINDEXED, \
1336 updated_at UNINDEXED, \
1337 record_kind, \
1338 tokenize = '{}'\
1339 )",
1340 table_key, tokenizer
1341 );
1342 let table = format!("fts_{table_key}");
1343 if self.is_read_only() {
1344 let reader = self.pool.reader()?;
1345 if !sqlite_table_exists(reader.conn(), &table)? {
1346 return Err(SqliteError::InvalidData(format!(
1347 "read-only database has no text-search table '{table}'; create and populate \
1348 it in a writable copy before opening the snapshot"
1349 )));
1350 }
1351 let map = text::rowid_map_table(&table);
1365 let map_exists = sqlite_table_exists(reader.conn(), &map)?;
1366 let marker_present = if map_exists {
1367 let state = text::rowid_map_state_table(&table);
1368 sqlite_table_exists(reader.conn(), &state)?
1369 && reader.conn().query_row(
1370 &format!(
1371 "SELECT EXISTS(SELECT 1 FROM {state} WHERE key = 'backfill' AND value = ?1)"
1372 ),
1373 rusqlite::params![text::ROWID_MAP_BACKFILL_COMPLETE],
1374 |row| row.get(0),
1375 )?
1376 } else {
1377 false
1378 };
1379 if !map_exists || !marker_present {
1380 warn_scan_fallback_once(&table);
1381 return Ok(Arc::new(text::Fts5TextSearch::new_scan_fallback(
1382 Arc::clone(&self.pool),
1383 self.is_file_backed,
1384 table_key.to_string(),
1385 )));
1386 }
1387 } else if !self.text_tables_ready.lock().contains(&table) {
1388 let writer = self.pool.autocommit_write_unit()?;
1389 if !self.text_tables_ready.lock().contains(&table) {
1392 writer.conn().execute_batch(&ddl)?;
1393 writer.conn().execute_batch(&text::rowid_map_ddl(&table))?;
1394 let marker_durable = ensure_fts_rowid_map_backfilled(
1395 writer.conn(),
1396 &table,
1397 &self.pool.write_admission(),
1398 )?;
1399 if marker_durable {
1402 self.text_tables_ready.lock().insert(table.clone());
1403 }
1404 }
1405 }
1406
1407 Ok(Arc::new(text::Fts5TextSearch::new(
1408 Arc::clone(&self.pool),
1409 self.is_file_backed,
1410 table_key.to_string(),
1411 )))
1412 }
1413
1414 pub fn blob_store(
1422 &self,
1423 config_root: Option<&Path>,
1424 floor_bytes: Option<u64>,
1425 ) -> Result<Arc<dyn khive_storage::BlobStore>, SqliteError> {
1426 let root = blob::resolve_blob_root(self.data_dir().as_deref(), config_root)?;
1427 let floor = floor_bytes.unwrap_or(blob::FsBlobStore::DEFAULT_FLOOR_BYTES);
1428 Ok(Arc::new(blob::FsBlobStore::new(root, floor)?))
1429 }
1430
1431 pub fn blob_store_read_only(
1436 &self,
1437 config_root: Option<&Path>,
1438 floor_bytes: Option<u64>,
1439 ) -> Result<Arc<dyn khive_storage::BlobStore>, SqliteError> {
1440 let root = blob::resolve_blob_root(self.data_dir().as_deref(), config_root)?;
1441 let floor = floor_bytes.unwrap_or(blob::FsBlobStore::DEFAULT_FLOOR_BYTES);
1442 Ok(Arc::new(blob::FsBlobStore::open_existing(root, floor)?))
1443 }
1444
1445 pub fn is_file_backed(&self) -> bool {
1447 self.is_file_backed
1448 }
1449
1450 pub fn is_read_only(&self) -> bool {
1453 self.pool.config().read_only
1454 }
1455
1456 pub fn data_dir(&self) -> Option<std::path::PathBuf> {
1459 self.path.as_ref()?.parent().map(|p| p.to_path_buf())
1460 }
1461
1462 pub fn ann_root(&self) -> Option<std::path::PathBuf> {
1470 ann_root_for(self.path.as_ref()?)
1471 }
1472
1473 pub fn pool(&self) -> &ConnectionPool {
1475 &self.pool
1476 }
1477
1478 pub fn database_owner_identity(
1481 &self,
1482 ) -> Result<DatabaseOwnerIdentity, DatabaseOwnerIdentityError> {
1483 self.pool.database_owner_identity()
1484 }
1485
1486 pub fn verify_database_owner(
1488 &self,
1489 expected: &DatabaseOwnerIdentity,
1490 ) -> Result<(), DatabaseOwnerIdentityError> {
1491 self.pool.verify_database_owner(expected)
1492 }
1493
1494 pub fn pool_arc(&self) -> Arc<ConnectionPool> {
1496 Arc::clone(&self.pool)
1497 }
1498}
1499
1500fn ann_root_for(path: &std::path::Path) -> Option<std::path::PathBuf> {
1505 let mut file = path.file_name()?.to_os_string();
1506 file.push(".ann");
1507 path.parent().map(|p| p.join(file))
1508}
1509
1510#[cfg(test)]
1511#[path = "backend/store_accessor_tests.rs"]
1512mod store_accessor_tests;
1513
1514#[cfg(test)]
1515#[path = "backend/store_accessor_index_tests.rs"]
1516mod store_accessor_index_tests;
1517
1518#[cfg(test)]
1519#[path = "backend_tests.rs"]
1520mod tests;
1521
1522#[cfg(test)]
1523#[path = "backend_admission_tests.rs"]
1524mod admission_tests;