pub struct Store {
pub root: PathBuf,
/* private fields */
}Expand description
Persistent knowledge store for a single mati project.
Wraps two SurrealKV trees:
knowledge— user-visible records (gotchas, files, decisions, …)sessions— analytics, hook events, compliance logs
All public methods are async; callers must be in a tokio context.
Fields§
§root: PathBufAbsolute path to ~/.mati/<slug>/
Implementations§
Source§impl Store
impl Store
Sourcepub async fn open(repo_root: &Path) -> Result<Self>
pub async fn open(repo_root: &Path) -> Result<Self>
Open (or create) both trees for the project rooted at repo_root.
Creates ~/.mati/<slug>/ if it does not exist.
If the search index is corrupt or schema-incompatible, it is wiped and
replaced with a fresh empty index. Store::index_needs_rebuild will
return true in that case — call Store::rebuild_search_index before
issuing any search queries, or use Store::open_and_rebuild which
handles this automatically.
Sourcepub async fn open_and_rebuild(repo_root: &Path) -> Result<Self>
pub async fn open_and_rebuild(repo_root: &Path) -> Result<Self>
Open the store and rebuild the search index from SurrealKV if needed.
This is the recommended entry point for the CLI and MCP server. It
combines Store::open with an automatic Store::rebuild_search_index
call when the index was corrupt or missing (C4). Search queries are safe
to issue immediately on the returned store.
Unlike Store::open, this eagerly initializes tantivy so corruption
can be detected and recovered from before any queries are issued.
Sourcepub fn index_needs_rebuild(&self) -> bool
pub fn index_needs_rebuild(&self) -> bool
True when the search index was corrupt or missing on open.
This flag reflects the state detected at open time and is not reset
after Store::rebuild_search_index completes. Use it only to decide
whether to call rebuild_search_index — not as a post-rebuild status.
Store::open_and_rebuild handles this automatically.
Sourcepub async fn rebuild_search_index(&self) -> Result<usize>
pub async fn rebuild_search_index(&self) -> Result<usize>
Rebuild the tantivy search index from scratch by scanning all
Record-containing namespaces in SurrealKV (C4).
Must be called on a store whose search index is empty — i.e. immediately
after Store::open detected a corrupt/missing index, before any writes.
Calling on a non-empty index will produce duplicate entries; use the
deduplication in Search::query_keys to tolerate this if it occurs.
Returns the total number of records committed to the index.
Sourcepub async fn get(&self, key: &str) -> Result<Option<Record>>
pub async fn get(&self, key: &str) -> Result<Option<Record>>
Read a record by key. Returns None if not found.
Sourcepub async fn put(&self, key: &str, record: &Record) -> Result<()>
pub async fn put(&self, key: &str, record: &Record) -> Result<()>
Write a record with the appropriate durability level.
Durability is derived from the key prefix via Durability::for_key.
Sourcepub async fn put_batch_kv_only(&self, records: &[(&str, &Record)]) -> Result<()>
pub async fn put_batch_kv_only(&self, records: &[(&str, &Record)]) -> Result<()>
Write multiple records to KV only, skipping the tantivy search index.
Use this during bulk init passes where search indexing would block the
critical path. Follow with Self::rebuild_search_index to update tantivy
from the same in-memory records without a KV round-trip.
Same durability semantics as Self::put_batch: at most 2 fsyncs.
Sourcepub fn mark_search_stale(&self)
pub fn mark_search_stale(&self)
Mark the search index as stale so the next Self::open_and_rebuild
call wipes and rebuilds it from KV.
Written by mati init after a cold init pass to defer the tantivy
indexing cost (~400ms on 27k records) to the first MCP server startup.
Best-effort: a write failure is silently discarded — the worst outcome
is that the search index contains stale data until the next full rebuild.
Sourcepub async fn put_batch(&self, records: &[(&str, &Record)]) -> Result<()>
pub async fn put_batch(&self, records: &[(&str, &Record)]) -> Result<()>
Write multiple records in a single transaction per durability class.
Records are grouped by their key prefix: all Immediate keys share one
transaction on knowledge (1 fsync), all Eventual keys share one on
sessions (1 fsync). The whole batch costs at most 2 fsyncs regardless
of how many records it contains — critical for Layer 0 bulk inserts.
Empty slice is a no-op. Mixed-durability batches are handled correctly.
Sourcepub async fn delete(&self, key: &str) -> Result<()>
pub async fn delete(&self, key: &str) -> Result<()>
Delete a record by key. No-op if the key does not exist.
Sourcepub async fn scan_prefix(&self, prefix: &str) -> Result<Vec<Record>>
pub async fn scan_prefix(&self, prefix: &str) -> Result<Vec<Record>>
Return all records whose key starts with prefix.
Prefix must use one of the known key namespaces so the correct tree is
selected. Unknown prefixes are scanned from knowledge.
Return order is not guaranteed. Callers that need a stable order must sort.
Sourcepub async fn scan_prefix_each<F>(&self, prefix: &str, callback: F) -> Result<()>
pub async fn scan_prefix_each<F>(&self, prefix: &str, callback: F) -> Result<()>
Scan records whose key starts with prefix, invoking callback for each.
Same tree routing and prefix semantics as Self::scan_prefix, but records
are deserialized and passed to callback one at a time rather than
collected into a Vec. Callers can begin processing (e.g. printing to
stdout) before the full scan completes, giving time-to-first-row
latency proportional to a single deserialization rather than the full
scan.
Return order is lexicographic (underlying KV order). Callers that need a different order must collect and sort after the fact.
Sourcepub async fn search(&self, text: &str, limit: usize) -> Result<Vec<Record>>
pub async fn search(&self, text: &str, limit: usize) -> Result<Vec<Record>>
Full-text BM25 search over all indexed records.
Calls tantivy for the top limit matching keys, then fetches each full
record from SurrealKV. Keys that tantivy returns but are not found in
the store (e.g. deleted since last commit) are silently skipped.
Returns results ordered by descending BM25 relevance score. Returns an
empty Vec when text is blank or limit is 0.
Sourcepub async fn search_scored(
&self,
text: &str,
limit: usize,
) -> Result<Vec<(f32, Record)>>
pub async fn search_scored( &self, text: &str, limit: usize, ) -> Result<Vec<(f32, Record)>>
Full-text BM25 search returning (score, Record) pairs.
Same semantics as Self::search but preserves the raw BM25
relevance score from tantivy. Used by mem_query text mode to
include relevance in the agent-facing response.
Sourcepub async fn get_raw_bytes(&self, key: &str) -> Result<Option<Vec<u8>>>
pub async fn get_raw_bytes(&self, key: &str) -> Result<Option<Vec<u8>>>
Read raw bytes by key. Returns None if the key does not exist.
Counterpart to Self::put_raw. Used for structural metadata,
enforcement events, and other non-Record values stored as raw bytes.
Sourcepub async fn put_batch_raw(&self, records: &[(&str, &[u8])]) -> Result<()>
pub async fn put_batch_raw(&self, records: &[(&str, &[u8])]) -> Result<()>
Write multiple raw-byte values in a single transaction per durability class.
Same batch semantics as Self::put_batch (at most 2 fsyncs for the
whole batch). Use for bulk structural writes like graph edge inserts.
Sourcepub async fn transact_knowledge(
&self,
ops: &[KnowledgeWriteOp<'_>],
) -> Result<()>
pub async fn transact_knowledge( &self, ops: &[KnowledgeWriteOp<'_>], ) -> Result<()>
Atomically commit multiple writes to the knowledge tree in a single transaction.
Supports mixed Record + raw byte writes. All keys MUST route to the
knowledge tree (Durability::Immediate). Returns an error if any key
routes to sessions.
Handles crash-fence + tantivy sync + write-seq after commit. Use this for mutation + audit atomic commit on knowledge-side commands.
Sourcepub async fn transact_sessions_raw(
&self,
entries: &[(&str, &[u8])],
) -> Result<()>
pub async fn transact_sessions_raw( &self, entries: &[(&str, &[u8])], ) -> Result<()>
Atomically commit multiple raw byte writes to the sessions tree in a single transaction.
All keys MUST route to the sessions tree (Durability::Eventual).
Returns an error if any key routes to knowledge.
Use this for mutation + audit atomic commit on session-side commands.
Sourcepub async fn scan_keys(&self, prefix: &str) -> Result<Vec<String>>
pub async fn scan_keys(&self, prefix: &str) -> Result<Vec<String>>
Return all keys whose prefix matches, without deserialising values.
Cheaper than Self::scan_prefix when only the key is needed (e.g.
graph edge loading, existence checks). Uses the SurrealKV iterator
key().user_key() path so value bytes are never read from disk.
Sourcepub fn history(&self, key: &str, limit: usize) -> Result<Vec<HistoryEntry>>
pub fn history(&self, key: &str, limit: usize) -> Result<Vec<HistoryEntry>>
Return version history for a single key, newest first.
Includes tombstones (deletions). Uses the tight upper bound key + \0
so adjacent keys never spill into the result set.
limit caps the number of entries returned; 0 means unlimited.
Sourcepub fn history_since(
&self,
key: &str,
since_ts: u64,
limit: usize,
) -> Result<Vec<HistoryEntry>>
pub fn history_since( &self, key: &str, since_ts: u64, limit: usize, ) -> Result<Vec<HistoryEntry>>
Return version history for a single key since since_ts (seconds),
newest first.
Timestamps are converted to nanoseconds for the SurrealKV range filter.
Sourcepub async fn records_since(
&self,
since_ts: u64,
limit: usize,
) -> Result<Vec<Record>>
pub async fn records_since( &self, since_ts: u64, limit: usize, ) -> Result<Vec<Record>>
Return all records updated since since_ts (seconds), newest first.
Scans every knowledge namespace (including dep:) and returns records
whose updated_at >= since_ts. Results are sorted by updated_at
descending with secondary sort by key for deterministic ordering.
Sourcepub async fn close(self) -> Result<()>
pub async fn close(self) -> Result<()>
Flush and close both trees, releasing the LOCK files.
Must be called before dropping Store if another process (or test) will
reopen the same database directory. SurrealKV holds an exclusive lock
for the lifetime of a Tree; reopening without closing first fails with
“already locked by another process”.
Sourcepub async fn flush_for_shutdown(&self)
pub async fn flush_for_shutdown(&self)
Best-effort durability flush for shutdown paths.
Calls SurrealKV’s flush_wal(sync=true) on both trees so every
previously-committed transaction reaches disk. Non-consuming and
&self — works through a shared Arc<RwLock<Graph>> read lock on
the daemon shutdown path where ownership cannot be reclaimed.
Necessary because SurrealKV’s Tree::Drop only fire-and-forget-spawns
core.close() onto the current tokio runtime; if the runtime is
shutting down (signal handler, main return) that spawned task may not
run before the process exits, losing buffered “Eventual” writes.
Errors are logged via tracing::warn! and not propagated — shutdown
paths must be infallible. Search index pending writes are committed
per Search::add_record/add_records call, so no separate flush
is needed here.
Sourcepub async fn ping(&self) -> Result<u64>
pub async fn ping(&self) -> Result<u64>
Ping the store. Writes a sentinel key and reads it back; returns round-trip latency in microseconds.
Used by mati ping and by hook fast-path availability checks.
Sourcepub fn read_write_seq(&self) -> u64
pub fn read_write_seq(&self) -> u64
Read the current knowledge write-sequence counter.
Returns 0 if the file does not exist or cannot be parsed — callers
treat 0 as “no valid cached snapshot” and recompute.
Sourcepub fn sessions_tree(&self) -> &Tree
pub fn sessions_tree(&self) -> &Tree
Direct access to the sessions tree for audit reads in tests.
Production code should use the key-routing methods (get, put_raw,
scan_keys) rather than accessing trees directly.
Trait Implementations§
Auto Trait Implementations§
impl !Freeze for Store
impl !RefUnwindSafe for Store
impl !UnwindSafe for Store
impl Send for Store
impl Sync for Store
impl Unpin for Store
impl UnsafeUnpin for Store
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<T> Fruit for T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more