pub struct WriteGuard<'a> { /* private fields */ }Expand description
Compound write guard returned by SharedDb::write.
Holds both the WAL mutex and the RwLock write guard. Fields are declared
in drop order — inner (RwLock) releases first, then _wal (WAL mutex)
— preserving the lock-release ordering required by the WAL I/O discipline.
§Lock order
Acquisition: wal_mu → cross-process LOCK → inner (RwLock write). The
cross-process lock is polled with no RwLock write guard held, so a peer
process holding it never stalls this process’s readers.
Release: cross-process LOCK (in Drop), then inner, then wal_mu
(RAII, struct field declaration order).
§Cross-process lock
Constructing the guard also takes the store’s advisory cross-process write
lock and refreshes, so mutations made through it land on top of every other
process’s commits. If the lock could not be taken within the caller’s wait
budget, the guard is still returned but every mutation on it fails with
GraphError::Busy — use SharedDb::write_with_wait to see the failure
up front instead.
Methods from Deref<Target = GraphDb<RealFs>>§
Sourcepub fn query_at(
&self,
commit: u64,
cypher: &str,
params: &BTreeMap<String, Value>,
) -> Result<ResultSet>
pub fn query_at( &self, commit: u64, cypher: &str, params: &BTreeMap<String, Value>, ) -> Result<ResultSet>
Run a read-only Cypher query against the graph as it existed at
commit — the “time-travel” / agent-replay query. Opens a temporal view
of this store’s directory at that commit and executes the read there.
The current instance is unaffected. Write statements are rejected (the
temporal view is read-only). commit is a 0-based WAL commit index;
commit == wal_commit_count (or open_at’s range) yields the newest
state. Prefer this over holding many historical instances open.
§Errors
GraphError::CommitOutOfRangeifcommitis past the WAL horizon.- A query error for a malformed or write query.
Sourcepub fn query_at_scoped(
&self,
commit: u64,
cypher: &str,
params: &BTreeMap<String, Value>,
scope: AsOfScope<'_>,
) -> Result<ResultSet>
pub fn query_at_scoped( &self, commit: u64, cypher: &str, params: &BTreeMap<String, Value>, scope: AsOfScope<'_>, ) -> Result<ResultSet>
Run a read-only Cypher query at commit, restricted by scope.
The graph is as of commit; the role definition is as it is
now, because roles.json is a sidecar and is never a WAL record — it
has no past version to read. A role’s keys and labels are resolved
against the commit-commit graph, so a role that may see a label sees
exactly the nodes that carried it then, and an explicit key that did
not exist yet resolves to nothing.
AsOfScope::RoleAndKeys intersects the two: a client allow-list can
only narrow what a role may see, never widen it.
Write statements are rejected, exactly as GraphDb::query_at rejects
them.
§Errors
GraphError::CommitOutOfRangeifcommitis outside the retained range; the error carries that range.GraphError::KeyNotFoundwith arole:prefix for an unknown role, orGraphError::Corruptwhenroles.jsonwas corrupt at open.- A query error for a malformed or write query.
Sourcepub fn query_at_scoped_in_namespace(
&self,
commit: u64,
cypher: &str,
params: &BTreeMap<String, Value>,
scope: AsOfScope<'_>,
namespace: &str,
) -> Result<ResultSet>
pub fn query_at_scoped_in_namespace( &self, commit: u64, cypher: &str, params: &BTreeMap<String, Value>, scope: AsOfScope<'_>, namespace: &str, ) -> Result<ResultSet>
As GraphDb::query_at_scoped, with namespace intersected into
whatever scope resolves to.
This is what a surface needs when a caller passes namespace beside a
role or a client mask on a time-travel read: AsOfScope names one
restriction, and the namespace is a second one that composes with it
rather than replacing it. The intersection is the never-widen rule — a
namespace can only narrow what the scope already allows — and both legs
are resolved against the graph as it was at commit.
AsOfScope::Namespace(ns) is still the way to ask for a namespace alone.
Sourcepub fn is_stale(&self) -> Result<bool>
pub fn is_stale(&self) -> Result<bool>
Whether the store on disk has moved ahead of (or out from under) this handle’s in-memory state.
True when the WAL’s length differs from this handle’s cursor — another process committed, or is mid-append — or when the snapshot file’s identity changed. Costs two metadata lookups and reads no file contents, so it is cheap enough for a read path to call.
Always false for an as-of view from GraphDb::open_at: such a view is
pinned to one commit and later commits are deliberately invisible to it.
Sourcepub fn refresh(&mut self) -> Result<u64>
pub fn refresh(&mut self) -> Result<u64>
Bring this handle up to date with every commit other processes have made, and return how many frames were applied.
The WAL tail is decoded from this handle’s cursor and applied through the same path the open replay uses, so rules fire and derived edges appear exactly as they would on a fresh open. Interners, id maps and indexes stay valid for the same reason.
A frame another process is still writing is left alone: a trailing partial frame is a wait, not a corruption, and the handle stays stale until that frame is complete. Nothing is written to disk, so a read-only handle can refresh freely.
When the snapshot file’s identity changed, or the WAL is shorter than this handle’s cursor, the WAL no longer continues our state — another process snapshotted or archived. The handle is then rebuilt from disk with the options it was opened with, and the return value is the number of frames in the new WAL.
Returns 0 for an as-of view, which never follows later commits.
§Errors
An error here leaves the handle degraded: it got partway through applying the tail, or partway through a reload, so its in-memory state no longer matches any point on disk. Further mutations are refused and the handle must be reopened. Nothing on disk was damaged — the store itself is fine, and a fresh open recovers it.
Sourcepub fn is_read_only(&self) -> bool
pub fn is_read_only(&self) -> bool
Whether this instance is a read-only as-of view.
Sourcepub fn reader(&self) -> ReaderSnapshot
pub fn reader(&self) -> ReaderSnapshot
Capture a lock-free reader snapshot of the current db state.
The read lock is held only for the duration of this call (to clone a
handful of Arc handles). Subsequent query operations run without any
lock.
Sourcepub fn total_wal_commits(&self) -> u64
pub fn total_wal_commits(&self) -> u64
Total number of WAL commits at the time [open_at] was called.
Returns 0 for normal (non-as-of) instances.
Sourcepub fn set_event_sink(&mut self, sink: Box<dyn Fn(MutationEvent) + Send + Sync>)
pub fn set_event_sink(&mut self, sink: Box<dyn Fn(MutationEvent) + Send + Sync>)
Install a post-commit hook. Replaces any previous sink.
The sink runs inside log_then_apply after a successful
durable commit, while the caller still holds &mut self. When this
database is behind a crate::SharedDb, that means the write
guard is held. The sink must never call read / write (or any
other method) on the same SharedDb — the RwLock is not
re-entrant and doing so deadlocks. The sink is Send + Sync;
std::sync::mpsc::Sender is not Sync and will not type-check.
Intended examples: std::sync::mpsc::SyncSender,
tokio::sync::mpsc::Sender, tokio::sync::broadcast::Sender
(non-blocking send), or Arc<Mutex<Vec<MutationEvent>>>.
Sourcepub fn has_event_sink(&self) -> bool
pub fn has_event_sink(&self) -> bool
Whether a post-commit event sink is currently installed.
Sourcepub fn set_fsync_policy(&mut self, p: FsyncPolicy)
pub fn set_fsync_policy(&mut self, p: FsyncPolicy)
Set WAL fsync cadence. Default FsyncPolicy::Strict.
Sourcepub fn fsync_policy(&self) -> FsyncPolicy
pub fn fsync_policy(&self) -> FsyncPolicy
Return the current WAL fsync cadence.
Sourcepub fn set_deferred_events_mode(&mut self, defer: bool)
pub fn set_deferred_events_mode(&mut self, defer: bool)
Enable or disable deferred event mode.
When true, event notifications (subscription DbEvents and legacy
MutationEvent sink calls) are buffered rather than fired immediately.
Call [flush_deferred_events] after the group fsync to deliver them,
or [discard_deferred_events] if the fsync failed and the group must
be treated as lost.
Sourcepub fn flush_deferred_events(&mut self)
pub fn flush_deferred_events(&mut self)
Fire all buffered events accumulated since [set_deferred_events_mode]
was set to true. Clears the buffer.
Called by the drain thread AFTER a successful group fsync, so subscribers observe only data that is durably on disk.
Sourcepub fn discard_deferred_events(&mut self)
pub fn discard_deferred_events(&mut self)
Discard all buffered events without firing them.
Called by the drain thread when a group fsync fails: the WAL has been truncated back to the pre-group offset, so the committed-but-unsynced ops must not be observable to subscribers.
Sourcepub fn set_degraded(&mut self)
pub fn set_degraded(&mut self)
Mark this database as degraded.
Called by the group-commit drain thread after a group fsync failure and
WAL truncation: the in-memory state is now ahead of the on-disk WAL, so
further mutations would deepen the divergence. All subsequent calls to
[log_then_apply_with] return Err until the database is reopened.
Sourcepub fn subscribe_rule(&mut self, rule_name: &str) -> Result<Subscription>
pub fn subscribe_rule(&mut self, rule_name: &str) -> Result<Subscription>
Subscribe to edge-fire and edge-retract events for one named rule.
Returns Err(GraphError::RuleNotFound) if rule_name is not
currently registered. Dropping the returned Subscription handle
unregisters the subscriber — no further events are queued, no
resources leak.
Sourcepub fn subscribe_all_rules(&mut self) -> Result<Subscription>
pub fn subscribe_all_rules(&mut self) -> Result<Subscription>
Subscribe to edge-fire and edge-retract events for all rules.
Returns Err(GraphError::ReadOnly) if called on an as-of instance —
as-of instances never commit, so distribute_events never runs and the
subscription would never deliver events.
Sourcepub fn subscribe_writes(&mut self) -> Result<Subscription>
pub fn subscribe_writes(&mut self) -> Result<Subscription>
Subscribe to write events: node insert/delete, prop set/remove.
Does not include edge-fire / edge-retract (rule-derived edge events).
Returns Err(GraphError::ReadOnly) if called on an as-of instance —
as-of instances never commit, so distribute_events never runs and the
subscription would never deliver events.
Sourcepub fn subscribe_query(&mut self, cypher: &str) -> Result<Subscription>
pub fn subscribe_query(&mut self, cypher: &str) -> Result<Subscription>
Subscribe to incremental Cypher query results.
Parses and plans cypher; rejects the query if the plan is not in the
allowlisted subset (see core_query::cypher::is_subscribable):
MATCH (n:Label) WHERE … RETURN … [LIMIT n]MATCH (a)-[r:TYPE]->(b) RETURN … [LIMIT n](exactly one hop)
SKIP is not supported — it shifts the result window on every commit, causing spurious Added/Removed churn for rows whose data never changed. Multi-hop Expand chains are not supported; each additional MATCH clause widens scope beyond the documented single-scan / single-hop subset.
After each successful commit, the plan is fully re-executed and the
result is diffed against the previous run. Added rows produce
DbEvent::QueryRowAdded; removed rows produce
DbEvent::QueryRowRemoved.
Full re-run per commit; use LIMIT to bound execution cost. The existing 1 M intermediate-row cap applies. Differential evaluation is roadmap / Phase 5.
Returns Err(GraphError::ReadOnly) if called on an as-of instance —
as-of instances never commit, so distribute_events never runs and the
subscription would never deliver events.
Returns Err(GraphError::QueryError) if the query fails to parse, plan,
or if the plan shape is not in the allowlist.
Sourcepub fn batch(&mut self) -> BatchBuilder<'_, F>
pub fn batch(&mut self) -> BatchBuilder<'_, F>
Start an atomic batch.
The returned BatchBuilder borrows self mutably until
BatchBuilder::commit. Builder methods queue ops only — no
validation, no WAL I/O. commit validates every queued op against
live state plus preceding ops in this batch (duplicate key inside
the batch is Err; an edge between two nodes created earlier in
the batch is valid; delete_node then insert of the same key is a
fresh identity). Validation never mutates the database. Any failure
leaves WAL bytes and in-memory state identical to before commit.
On success, one WalRecord::Batch frame is appended (one fsync)
and each inner record is applied in order so rules fire per record.
An empty batch, or a batch of only no-ops, writes zero WAL bytes.
Rule-window limitation: batch validation cannot see edges that a
rule created earlier in the same batch will derive at apply time, so
a delete_edge / insert_edge in that window is silently no-oped
where sequential calls would return Err(RuleOwned). State integrity
is unaffected (idempotent apply, provenance intact). Create rules in
their own batch, or sequentially, when later ops may touch derived
edges.
Sourcepub fn write_batch<C>(&mut self, build: C) -> Result<(usize, usize)>where
C: FnOnce(&mut BatchBuilder<'_, F>),
pub fn write_batch<C>(&mut self, build: C) -> Result<(usize, usize)>where
C: FnOnce(&mut BatchBuilder<'_, F>),
Closure-style atomic write batch.
Equivalent to calling GraphDb::batch, invoking build to queue ops,
then committing. All ops queued inside build are validated in order and
committed as a single WalRecord::Batch frame (one fsync). Rules fire
once per inner record, in order, after commit — semantically identical to
sequential single-op writes.
Error semantics — validate-then-apply. build queues ops without
touching the database. BatchBuilder::commit validates every op against
live state plus earlier ops in this batch before writing anything. If op N
fails validation (duplicate key, unknown key, rule-owned edge, …) the
entire batch is rejected: no WAL bytes are written and no in-memory state
changes. The database is identical to its state before write_batch was
called.
Atomicity is crash-level, NOT isolation-level. On replay after a crash,
a partial (torn) Batch frame applies NONE of its ops — the frame is
either fully applied or not at all. However, while applying a committed
batch, concurrent readers may observe intermediate states as ops are applied
sequentially in memory. There is no interactive transaction isolation in v1.
This is documented as “crash-atomic write batches; no interactive
transactions or read isolation.”
Returns (nodes_inserted, edges_inserted). An empty or all-noop batch
writes zero WAL bytes and returns (0, 0).
§Example
let (nodes, edges) = db.write_batch(|b| {
b.insert_node("Person", "alice", vec![("age".into(), Value::Int(30))]);
b.insert_node("Person", "bob", vec![]);
b.insert_edge("KNOWS", "alice", "bob");
b.set_prop("alice", "role", Value::Str("admin".into()));
b.delete_node("old_key");
})?;
// One fsync; on crash replay: all five ops land or none do.Sourcepub fn ingest(
&mut self,
label: &str,
rows: Vec<BTreeMap<String, Value>>,
opts: &IngestOptions,
) -> Result<IngestReport>
pub fn ingest( &mut self, label: &str, rows: Vec<BTreeMap<String, Value>>, opts: &IngestOptions, ) -> Result<IngestReport>
Insert rows as nodes of label. One call is one atomic batch:
auto-declared KeyMatch rules (if any) first, then the accepted node
inserts, so incremental fire sees the new rules. Per-row key problems
are collected in IngestReport::row_errors and skipped; a commit
Err means nothing was applied.
Auto-FK rule names are auto_fk_<src_label_lowercase>_<field> so
distinct source labels sharing an FK field each get their own rule.
Sourcepub fn ingest_with_edges(
&mut self,
label: &str,
rows: Vec<BTreeMap<String, Value>>,
opts: &IngestOptions,
edges: &[(String, String, String)],
) -> Result<IngestReport>
pub fn ingest_with_edges( &mut self, label: &str, rows: Vec<BTreeMap<String, Value>>, opts: &IngestOptions, edges: &[(String, String, String)], ) -> Result<IngestReport>
[ingest] plus user edges in the same previewed WAL batch.
A failing edge rejects the whole request; nothing is applied.
Sourcepub fn ingest_json(
&mut self,
label: &str,
json: &str,
opts: &IngestOptions,
) -> Result<IngestReport>
pub fn ingest_json( &mut self, label: &str, json: &str, opts: &IngestOptions, ) -> Result<IngestReport>
Parse json as an array of objects and ingest via GraphDb::ingest.
JSON null fields are silently omitted (not stored, not a row error).
Nested objects and arrays-of-objects are a per-row error (row skipped).
Parse failures and a top-level value that is not an array of objects
return GraphError::IngestError.
Sourcepub fn commit_group(
&mut self,
groups: Vec<Vec<BatchOp>>,
) -> (Vec<Result<(usize, usize)>>, Option<GraphError>)
pub fn commit_group( &mut self, groups: Vec<Vec<BatchOp>>, ) -> (Vec<Result<(usize, usize)>>, Option<GraphError>)
Commit multiple op-batches as a group: each submission gets its own
WAL Batch frame, but there is exactly one Fs::sync for the whole
group (under Strict / Batched policy; Relaxed skips all syncs).
§Durability semantics
A crash before the group fsync may lose all submissions in the group. A crash after the group fsync preserves all of them. No submission is ever torn: each WAL frame is either fully applied on replay or dropped in its entirety (CRC-protected frame boundaries).
Events and subscription notifications fire per-submission immediately
after apply, which may be before the group fsync. From a subscriber’s
perspective this is equivalent to the Relaxed durability window.
Submitters using [SharedDb::submit_batch] only unblock after the group
fsync, so from their perspective durability is fully guaranteed.
§MVCC interplay
Each submission records its own CommitDelta; the fold-every-K counter
increments per submission (not per group), preserving existing reader
snapshot semantics.
§Returns
One Result<(nodes_inserted, edges_inserted)> per input group element,
in order. Failures are per-submission (validation errors); the group
fsync error (if any) is returned as the second tuple element.
Sourcepub fn commit_group_nosync(
&mut self,
groups: Vec<Vec<BatchOp>>,
) -> Vec<Result<(usize, usize)>> ⓘ
pub fn commit_group_nosync( &mut self, groups: Vec<Vec<BatchOp>>, ) -> Vec<Result<(usize, usize)>> ⓘ
Like [commit_group] but skips the group fsync entirely.
Used by the drain thread to apply submissions under the write lock and
then perform the single fsync OUTSIDE the lock (via
core_storage::sync_wal_at), reducing the write-lock hold time visible
to concurrent readers.
pub fn insert_node( &mut self, label: &str, key: &str, props: Vec<(String, Value)>, ) -> Result<()>
pub fn insert_edge( &mut self, edge_type: &str, src_key: &str, dst_key: &str, ) -> Result<bool>
pub fn set_prop(&mut self, key: &str, field: &str, value: Value) -> Result<()>
Sourcepub fn set_props(
&mut self,
key: &str,
props: Vec<(String, Value)>,
) -> Result<()>
pub fn set_props( &mut self, key: &str, props: Vec<(String, Value)>, ) -> Result<()>
Set several properties on one live node in a single WAL commit.
Every per-property check set_prop runs — view-owned
names, live key, the ns immutability rule and its type — is evaluated
for the whole list before any record is logged. The first refusal
returns and the node is unchanged. An empty list writes nothing.
Sourcepub fn remove_prop(&mut self, key: &str, field: &str) -> Result<bool>
pub fn remove_prop(&mut self, key: &str, field: &str) -> Result<bool>
Remove a property. Returns Ok(false) (and does not log) if the field
is already absent. Unknown or tombstoned keys are Err(KeyNotFound).
Sourcepub fn delete_edge(
&mut self,
edge_type: &str,
src_key: &str,
dst_key: &str,
) -> Result<bool>
pub fn delete_edge( &mut self, edge_type: &str, src_key: &str, dst_key: &str, ) -> Result<bool>
Delete a user edge. Returns Ok(false) (and does not log) if the edge
is absent. Unknown keys are Err(KeyNotFound). Rule-owned edges — in
provenance, or a pair a live rule would derive — are Err(RuleOwned)
(the rule would just put the edge back; delete or change the rule).
Sourcepub fn delete_node(&mut self, key: &str) -> Result<DeleteReport>
pub fn delete_node(&mut self, key: &str) -> Result<DeleteReport>
Delete a live node. Unknown or already-tombstoned keys are
Err(KeyNotFound) and are not logged. Validation runs before the WAL
write; apply of a logged DeleteNode for an already-tombstoned key
(crash window) is a clean no-op.
Returns a DeleteReport with counts of manual and derived edges
removed (computed from live state before the deletion is applied).
Sourcepub fn rename_node(&mut self, old: &str, new: &str) -> Result<()>
pub fn rename_node(&mut self, old: &str, new: &str) -> Result<()>
Rename a live node’s key. The dense id (and therefore all edges, props, history, and last-change tracking) is unaffected.
Returns Err(KeyNotFound) if old is not a live key.
Returns Err(DuplicateKey) if new is already live.
Sourcepub fn ivf_dst_drift(&self, rule: &str) -> Option<u64>
pub fn ivf_dst_drift(&self, rule: &str) -> Option<u64>
Return the IVF drift counter for the dst-side candidate index of rule.
None if the rule does not exist or is not approximate.
The drift counter increments on IVF insert/remove after the last fit.
When dst-side drift exceeds core_rules::IVF_DRIFT_REBUILD, apply
WAL-logs RebuildRule as a second commit (rebuild resets the counter).
Sourcepub fn create_rule(&mut self, def: RuleDef) -> Result<()>
pub fn create_rule(&mut self, def: RuleDef) -> Result<()>
Validate and WAL-log a new rule, then backfill derived edges inside apply. Validation and duplicate-name check run before logging so invalid rules never enter the WAL.
Sourcepub fn builds_in_progress(&self) -> Vec<BuildProgress>
pub fn builds_in_progress(&self) -> Vec<BuildProgress>
Rules whose vector index is still being built, in name order.
The same list GraphDb::stats reports per rule in building.
After a clean open this includes a build a snapshot cut short, so
serve’s ticker can pump it without a write.
Sourcepub fn pump_index_build(&mut self) -> Result<Vec<BuildProgress>>
pub fn pump_index_build(&mut self) -> Result<Vec<BuildProgress>>
Advance any vector index still building and backfill each rule that finishes. Returns what is still outstanding.
A map lookup when nothing is pending, so it is cheap to call on a timer.
One write lock and at most core_rules::HNSW_BUILD_BATCH vector
inserts per pending rule per call, so a caller can drive a large build
to completion without ever holding the lock for more than a slice.
A rule that finishes here is backfilled through the same
WalRecord::RebuildRule second commit that IVF drift already uses, so
its derived edges are produced by GraphDb::rebuild_rule’s code path
and appear all at once.
Every ordinary write pumps one slice on its own (see the post-commit
hook in log_then_apply_with), so this is for quiescent stores and for
operators who want the build finished before traffic arrives.
Sourcepub fn pump_index_build_reporting(
&mut self,
) -> Result<(Vec<BuildProgress>, Vec<BuildProgress>)>
pub fn pump_index_build_reporting( &mut self, ) -> Result<(Vec<BuildProgress>, Vec<BuildProgress>)>
GraphDb::pump_index_build, also reporting the builds that this
call finished, so a progress display can say so.
A build can be registered and completed inside a single call — that is what a mid-build snapshot looks like on reopen, where the index scan finishes the graph and only the backfill is outstanding — and the outstanding list alone cannot show that anything happened.
Sourcepub fn delete_rule(&mut self, name: &str) -> Result<()>
pub fn delete_rule(&mut self, name: &str) -> Result<()>
WAL-log rule deletion. Returns RuleNotFound if the rule does not exist.
Sourcepub fn suggest_rules(&self) -> Vec<RuleSuggestion>
pub fn suggest_rules(&self) -> Vec<RuleSuggestion>
Profile the database and suggest linking rules with previewed edge counts.
Uses the default seed (core_rules::SUGGEST_DEFAULT_SEED) for deterministic
sampling. Suggestions are sorted by estimated edge count (descending).
NO auto-accept — call GraphDb::create_rule explicitly to apply.
Sourcepub fn suggest_rules_seeded(&self, seed: u64) -> Vec<RuleSuggestion>
pub fn suggest_rules_seeded(&self, seed: u64) -> Vec<RuleSuggestion>
Like [suggest_rules] but with a caller-supplied RNG seed for
reproducibility. Same seed + same data = identical output.
Sourcepub fn suggest_rules_with_config(
&self,
config: &SuggestConfig,
seed: u64,
) -> SuggestReport
pub fn suggest_rules_with_config( &self, config: &SuggestConfig, seed: u64, ) -> SuggestReport
[suggest_rules_seeded] with a fully custom [SuggestConfig].
Returns a core_rules::SuggestReport that includes both the candidate list
and a truncated flag indicating whether the global budget fired before all
candidates were evaluated.
Sourcepub fn rebuild_rule(&mut self, name: &str) -> Result<()>
pub fn rebuild_rule(&mut self, name: &str) -> Result<()>
Recompute a rule’s derived edges from scratch. WAL-logged so un-trip plus later mutations replay identically (rebuild is a pure function of state).
Only exit from the tripped latch: if the full desired set fits the
budget, it is applied completely and tripped clears; if it still
exceeds the budget, provenance is left untouched and tripped stays
true. Counts as a fire evaluation (see RuleStats::fires).
Unknown rule → RuleNotFound, nothing logged.
Sourcepub fn create_view(&mut self, def: ViewDef) -> Result<()>
pub fn create_view(&mut self, def: ViewDef) -> Result<()>
Register a new materialized property view, backfill its values for all existing nodes, and WAL-log the definition.
§Errors
ReadOnly: called on an as-of instance.RuleInvalid: name collision, view_prop collision, or invalid def.
Sourcepub fn delete_view(&mut self, name: &str) -> Result<()>
pub fn delete_view(&mut self, name: &str) -> Result<()>
Remove a named view and delete its values from every node.
§Errors
ReadOnly: called on an as-of instance.RuleNotFound: view does not exist.
Sourcepub fn enable_fulltext(&mut self, label: &str, field: &str) -> Result<()>
pub fn enable_fulltext(&mut self, label: &str, field: &str) -> Result<()>
Enable full-text indexing for all nodes of label on property field.
After this call, every subsequent write to (label, field) is reflected
in the index incrementally. Existing nodes are backfilled immediately.
The declaration is persisted as a WAL record; the index itself is rebuilt
from scratch on re-open (no snapshot format changes).
§Errors
GraphError::ReadOnly: called on an as-of instance.GraphError::RuleInvalid:(label, field)is already indexed.
Sourcepub fn disable_fulltext(&mut self, label: &str, field: &str) -> Result<()>
pub fn disable_fulltext(&mut self, label: &str, field: &str) -> Result<()>
Disable full-text indexing for (label, field) and drop its postings.
§Errors
GraphError::ReadOnly: called on an as-of instance.GraphError::RuleNotFound:(label, field)is not currently indexed.
Sourcepub fn is_fulltext_enabled(&self, label: &str, field: &str) -> bool
pub fn is_fulltext_enabled(&self, label: &str, field: &str) -> bool
Whether (label, field) is currently indexed for full-text search.
Sourcepub fn fulltext_pairs(&self) -> Vec<(String, String)>
pub fn fulltext_pairs(&self) -> Vec<(String, String)>
Every (label, field) pair with a live full-text index, sorted.
Note that GraphDb::search is keyed by field alone — a pair only
declares which nodes are indexed, so callers that want to search
everything indexed should query each distinct field once.
Sourcepub fn enable_index(&mut self, label: &str, field: &str) -> Result<()>
pub fn enable_index(&mut self, label: &str, field: &str) -> Result<()>
Enable an equality index for all nodes of label on scalar property
field. Subsequent WHERE n.field = value lookups become O(matches)
instead of an O(N_label) scan. Existing nodes are backfilled; the
declaration persists via WAL and the postings rebuild on re-open.
§Errors
GraphError::ReadOnly: called on an as-of instance.GraphError::RuleInvalid:(label, field)is already indexed.
Sourcepub fn disable_index(&mut self, label: &str, field: &str) -> Result<()>
pub fn disable_index(&mut self, label: &str, field: &str) -> Result<()>
Disable the equality index for (label, field) and drop its postings.
§Errors
GraphError::ReadOnly: called on an as-of instance.GraphError::RuleNotFound:(label, field)is not currently indexed.
Sourcepub fn is_index_enabled(&self, label: &str, field: &str) -> bool
pub fn is_index_enabled(&self, label: &str, field: &str) -> bool
Whether (label, field) currently has an equality index.
Sourcepub fn search(&self, field: &str, query: &str) -> Vec<(String, f64)>
pub fn search(&self, field: &str, query: &str) -> Vec<(String, f64)>
Search a full-text-indexed field.
Returns (node_key, match_count) pairs sorted by match_count descending,
ties broken by key (lexicographic). Tombstoned nodes are excluded.
Query syntax:
- Space-separated terms are AND’d:
"foo bar"requires both. ORbetween terms forms disjunction:"foo OR bar"matches either.- Trailing
*on a term is a prefix match:"rust*"matchesrustlang,rusty. ANDkeyword is accepted explicitly and is the default.- Tokenization is unicode-alphanumeric (same as index time); case-insensitive.
Unindexed field: returns Ok(vec![]) if field is not indexed.
Pin: this is the documented, tested, stable behavior for v1.
Memory / performance: O(postings) lookup; no scan. The index is in-memory and proportional to total indexed text across all enabled fields.
v2 grammar: supports "phrase", -negation, prefix*, OR, AND.
Results are BM25-scored (k1=1.2, b=0.75) and sorted by score descending,
key ascending for deterministic tiebreaking.
Sourcepub fn search_top(
&self,
field: &str,
query: &str,
k: usize,
) -> Vec<(String, f64)>
pub fn search_top( &self, field: &str, query: &str, k: usize, ) -> Vec<(String, f64)>
search, stopping at the k best hits.
Same ranking and the same deterministic tiebreak, but the index drops
everything past k before any key is resolved, so a caller that wants
the top few out of a field that matched thousands does not pay to
materialise and re-sort the tail. k == 0 means no limit, exactly as
search behaves.
The BM25 scoring itself is not bounded by k — every candidate is
scored either way — so this trims the resolve and the sort, not the
search.
Sourcepub fn search_hybrid(
&self,
text_field: &str,
query_text: &str,
vector_field: &str,
query_vec: &[f64],
label: Option<&str>,
k: usize,
) -> Vec<(String, f64)>
pub fn search_hybrid( &self, text_field: &str, query_text: &str, vector_field: &str, query_vec: &[f64], label: Option<&str>, k: usize, ) -> Vec<(String, f64)>
Hybrid search: Reciprocal Rank Fusion (RRF) over fulltext + vector results.
Takes up to 4*k fulltext hits for (text_field, query_text) and up to
4*k vector hits for (vector_field, query_vec, min=0.0), then fuses
them with RRF using a fixed constant of 60.
score(d) = Σ 1 / (60 + rank_i(d)) (rank 1-based per list)Returns the top k nodes by fused score, ties broken by node key
ascending (deterministic).
§Vector leg fallback
When query_vec is empty the vector leg is skipped entirely and
results are ranked by the text list alone through the same RRF path
(each text result scores 1/(60 + rank) from that single list).
When label is None, the vector leg always returns empty results.
Internally label is mapped to "", which does not match any rule-created
HNSW index (all such indexes are keyed to a specific non-empty label), and
the brute-force fallback finds no nodes with an empty label. The fused
ranking is therefore text-only in this case.
Sourcepub fn get_view_prop(&self, key: &str, view_prop: &str) -> Option<Value>
pub fn get_view_prop(&self, key: &str, view_prop: &str) -> Option<Value>
Return the current view-maintained value of view_prop for node key.
Equivalent to get_prop but documents that it reads a view-managed column.
Sourcepub fn pagerank(&self, config: &PageRankConfig) -> PageRankReport
pub fn pagerank(&self, config: &PageRankConfig) -> PageRankReport
Run PageRank over the unified topology (manual + derived edges).
Returns a [PageRankReport] with scores sorted descending (ties: key
ascending). Set config.edge_type to restrict to one edge type.
config.converged is true only when the power iteration converged
within config.max_iters and within any time budget.
Sourcepub fn connected_components(&self, config: &WccConfig) -> WccReport
pub fn connected_components(&self, config: &WccConfig) -> WccReport
Weakly-connected components over the unified topology (treated as undirected regardless of how edges were inserted).
Component IDs are the key of the smallest member in the component (deterministic). Result sorted by (component_id, key).
Sourcepub fn degree_centrality(&self, config: &DegreeConfig) -> DegreeReport
pub fn degree_centrality(&self, config: &DegreeConfig) -> DegreeReport
Degree centrality for every live node.
direction: AlgoDir::Out = out-degree, AlgoDir::In = in-degree,
AlgoDir::Both = out + in (total directed degree).
For one-shot ranking use this; for a live property updated on every
write, create a Degree materialized view instead (see docs/site/algorithms.md).
Sourcepub fn communities(&self, config: &LouvainConfig) -> CommunityReport
pub fn communities(&self, config: &LouvainConfig) -> CommunityReport
Louvain community detection over the unified topology (undirected).
See crate::algo::LouvainConfig for edge-type/weight/label
restriction and crate::algo::CommunityReport for the shape of the
result (communities sorted size-desc, then smallest member key asc).
Sourcepub fn write_scores(
&mut self,
prop_name: &str,
scores: &[(String, f64)],
) -> Result<()>
pub fn write_scores( &mut self, prop_name: &str, scores: &[(String, f64)], ) -> Result<()>
Write a vector of (node_key, score) pairs as prop_name on each node,
atomically via a single write-batch (one WAL frame, one fsync).
§Errors
GraphError::ReadOnly: called on an as-of instance.GraphError::RuleInvalid:prop_nameis managed by an existing view (collision check mirrorscreate_view).GraphError::KeyNotFound: a key inscoresdoes not exist as a live node.
Sourcepub fn get_prop(&self, key: &str, field: &str) -> Option<Value>
pub fn get_prop(&self, key: &str, field: &str) -> Option<Value>
Return the value of field for the node with key key, or None if
the node or field is absent. Reads through the overlay-over-base
ColumnsView, materialising base values on demand (zero heap cost for
overlay hits; one clone per base hit).
pub fn has_node(&self, key: &str) -> bool
Sourcepub fn namespaces(&self) -> Vec<String>
pub fn namespaces(&self) -> Vec<String>
Every namespace with at least one live node, in name order.
["default"] on any store that has never named a namespace, including
an empty one: a store is always at least its default namespace.
Sourcepub fn namespace_of(&self, key: &str) -> Option<String>
pub fn namespace_of(&self, key: &str) -> Option<String>
The namespace of key, or None when the key names no live node.
Sourcepub fn mask_for_namespace(&self, namespace: &str) -> NodeMask
pub fn mask_for_namespace(&self, namespace: &str) -> NodeMask
Every live node in namespace, as a visibility mask.
Built off node_ns on whichever handle this is, so on a temporal handle
it is the namespace’s membership at that commit. A name no node uses
gives an empty mask — a namespace scope never widens.
Sourcepub fn mask_for_role(&self, role: &str) -> Result<NodeMask>
pub fn mask_for_role(&self, role: &str) -> Result<NodeMask>
Resolve a role to a node-visibility mask against the current graph state.
Returns Err when:
roles.jsonwas present but corrupt at open (poisoned state), orroledoes not match any defined role name.
The mask union is: explicit keys (unknown keys silently ignored) plus
all live nodes carrying any label in labels that also pass the role’s
visible_where predicate, if it
has one. Label resolution is live — new nodes of an allowed label are
visible without re-applying the schema, and a property edited out of the
predicate takes its node out of the mask on the next read. An empty
union = empty mask = sees nothing.
This is the one resolver every read path calls, live and as-of alike, so the predicate applies everywhere at once. On an as-of handle the role definition is the current one and the graph is the historical one: the predicate is evaluated against the property values at the commit being read.
The result is memoised per (role, commit_seq), so a scoped reader
between two writes resolves the role once. See
RoleMaskCache for why that cannot go
stale.
Sourcepub fn roles(&self) -> Vec<RoleDef>
pub fn roles(&self) -> Vec<RoleDef>
Return the current list of role definitions.
Returns an empty list when no roles are defined or when roles.json
was corrupt at open (check mask_for_role for
the fail-loud error in that case).
Sourcepub fn write_batch_authz(
&mut self,
authz: Option<&WriteAuthz>,
ops: Vec<BatchOp>,
) -> Result<(usize, usize)>
pub fn write_batch_authz( &mut self, authz: Option<&WriteAuthz>, ops: Vec<BatchOp>, ) -> Result<(usize, usize)>
Execute ops with optional role-scoped write authorization.
None→ full authority, identical towrite_batch(zero-cost bypass of all authz checks).Some(authz)→ the decision table is evaluated per-op BEFORE any WAL record is built. A denial returns an error with no WAL frame written (all-or-nothing at the authz boundary, then at the MutPreview boundary).
See the plan’s “authz decision table” section for the full semantics.
Sourcepub fn query_write_authz(
&mut self,
role: &str,
cypher: &str,
params: &BTreeMap<String, Value>,
) -> Result<ResultSet>
pub fn query_write_authz( &mut self, role: &str, cypher: &str, params: &BTreeMap<String, Value>, ) -> Result<ResultSet>
Execute a Cypher write statement with role-scoped write authorization.
Resolves scope + mask from self.roles inside the call (same write-guard
lifetime as execution, satisfying §5 lock discipline). The resolved
WriteAuthz is stored as pending_write_authz for the duration of the
call so that all inner batch.commit() calls are authz-checked.
MERGE is handled specially: the MERGE scope precondition (§3.3) is
checked in exec_merge BEFORE has_node to close the §6.2
timing-oracle item (hidden ≡ absent for unscoped roles).
Roles with write: None (v1 behavior) → RoleWriteDenied with
“this endpoint is not permitted”.
Sourcepub fn ingest_with_edges_authz(
&mut self,
role: &str,
label: &str,
rows: Vec<BTreeMap<String, Value>>,
opts: &IngestOptions,
edges: &[(String, String, String)],
) -> Result<IngestReport>
pub fn ingest_with_edges_authz( &mut self, role: &str, label: &str, rows: Vec<BTreeMap<String, Value>>, opts: &IngestOptions, edges: &[(String, String, String)], ) -> Result<IngestReport>
Execute a /ingest request with role-scoped write authorization.
Resolves the role’s WriteScope and NodeMask inside this call (same
write-guard lifetime as the mutation, satisfying §5 lock discipline).
Sets pending_write_authz for the duration of the call so that the
commit_ingest → commit_logged_batch path picks up the authz context
and evaluates the decision table per-op before any WAL write.
§7.3: roles with empty create_labels will see every InsertNode op
denied by the decision table with the appropriate §4.3 scope reason;
no special HTTP-layer check is needed.
Roles with write: None return RoleWriteDenied with
“writes are not permitted” (byte-identical to v1 blanket 403).
Sourcepub fn query_masked(
&self,
cypher: &str,
params: &BTreeMap<String, Value>,
mask: &NodeMask,
) -> Result<ResultSet>
pub fn query_masked( &self, cypher: &str, params: &BTreeMap<String, Value>, mask: &NodeMask, ) -> Result<ResultSet>
Execute a read-only Cypher query with a node visibility mask.
Only nodes whose key is in mask are accessible: label scans, key
lookups, and neighbor expansions all respect the mask. Edges where
either endpoint is hidden are silently dropped.
Returns Err with a “masked queries are read-only” message when
cypher is a write statement (CREATE / MERGE / MATCH…SET / DELETE).
pub fn node_ref(&self, key: &str) -> Option<NodeRef<'_, F>>
Sourcepub fn neighborhood_masked(
&self,
key: &str,
depth: u32,
edge_types: Option<&[&str]>,
dir: Dir,
mask: &NodeMask,
) -> Option<ResultSet>
pub fn neighborhood_masked( &self, key: &str, depth: u32, edge_types: Option<&[&str]>, dir: Dir, mask: &NodeMask, ) -> Option<ResultSet>
BFS neighborhood expansion restricted to visible nodes in mask.
Hidden nodes are never used as traversal intermediaries in either
[MaskMode::Omit] or [MaskMode::Stub] — a visible node reachable
only through a hidden node will not appear in results.
In [MaskMode::Stub] mode, hidden nodes that are direct neighbours of
a visited visible node are appended to the result as stub rows
(label column is null, same key+depth columns as visible rows).
They are NOT added to the BFS frontier.
Returns None when key does not exist (caller should 404).
SECURITY: role-token callers always pass an Omit-mode mask, so stub rows are never produced on the role path.
Sourcepub fn node_info(&self, key: &str) -> Option<NodeInfo>
pub fn node_info(&self, key: &str) -> Option<NodeInfo>
Live node’s key, label, and columnar props. Unknown or tombstoned → None.
Sourcepub fn node_info_masked(
&self,
key: &str,
mask: &NodeMask,
) -> Option<MaskedNodeResult>
pub fn node_info_masked( &self, key: &str, mask: &NodeMask, ) -> Option<MaskedNodeResult>
Look up a node with mask awareness.
| Key state | Omit mode | Stub mode |
|---|---|---|
| does not exist | None (→ 404) | None (→ 404) |
| exists, visible | Some(Visible) | Some(Visible) |
| exists, hidden | None (→ 404) | Some(Restricted) |
SECURITY: only call from client-mask (full-token) paths.
Role-token paths must use [node_info] after an explicit visibility check.
Sourcepub fn node_edges_masked(
&self,
key: &str,
mask: &NodeMask,
) -> Result<Vec<MaskedEdge>>
pub fn node_edges_masked( &self, key: &str, mask: &NodeMask, ) -> Result<Vec<MaskedEdge>>
Get edges for key with mask-aware hidden-endpoint handling.
- Omit mode: edges to hidden endpoints are excluded (same as role-path filtering).
- Stub mode: edges to hidden endpoints are included;
src_restricted/dst_restrictedistruefor each hidden endpoint.
Unknown key → GraphError::KeyNotFound.
SECURITY: only call from client-mask (full-token) paths.
Sourcepub fn node_edges(&self, key: &str) -> Result<Vec<EdgeInfo>>
pub fn node_edges(&self, key: &str) -> Result<Vec<EdgeInfo>>
Every directed edge incident on key, both directions, every etype.
Walk is topology.etypes() × {Out, In} × neighbors(). derived is
membership in RuleEngine::provenance_touching (O(degree) via the
Plan-8 by_node index). Sorted by (edge_type, src_key, dst_key).
Unknown key → GraphError::KeyNotFound.
Sourcepub fn backup_to(&self, dest: &Path) -> Result<BackupReport>
pub fn backup_to(&self, dest: &Path) -> Result<BackupReport>
Copy this store to dest as a consistent, verified snapshot.
Copies every durable file in the database directory — snapshot.bin,
wal.bin, all wal.<N>.archive files, wal.floor, wal.genesis, and
roles.json — into a freshly created dest directory using OS-level
copy calls (no large in-process buffers).
§Consistency guarantee
The guarantee is process-local: the caller holds &self, which
prevents any concurrent writer in the same process from modifying
the files during the copy. Running mushroomdb backup against a
directory that is concurrently being written by another process (e.g.
mushroomdb serve) is unsafe — the copy can be torn. The post-copy
verified: true result reduces but does not eliminate the risk of a
silent corrupt backup (CRC catches many bit-flips; it cannot catch a
consistent mid-write snapshot).
The safe path for a live-served store is POST /backup on the HTTP
server. That handler acquires the read lock on the shared database
before calling this method, which is the correct cross-process
synchronisation point because the server is the single process writing
the files.
After copying, opens the destination read-only and runs the CRC section
verifier (verify_snapshot) to confirm byte-for-byte integrity.
BackupReport::verified reflects whether both checks passed.
Returns Err when self is not backed by a RealFs (e.g. SimFs).
Sourcepub fn all_nodes_for_export(&self) -> Vec<NodeInfo>
pub fn all_nodes_for_export(&self) -> Vec<NodeInfo>
All live nodes, sorted by key (deterministic).
Reads base + WAL overlay. Tombstoned nodes are excluded.
Sourcepub fn all_edges_for_export(&self) -> Vec<ExportEdge>
pub fn all_edges_for_export(&self) -> Vec<ExportEdge>
All directed edges, sorted by (edge_type, src, dst). Each edge appears once.
Derived edges carry derived: true and the creating rule’s name in rule.
Manual edges carry derived: false and rule: None.
weight is the creating rule’s weight_prop value read off the edge
(numeric only), mirroring the convention used by GraphDb::explain
and GraphDb::weighted_edges. Deterministic across runs on the same
store state.
Sourcepub fn edge_type_census(&self) -> Vec<EdgeTypeCensus>
pub fn edge_type_census(&self) -> Vec<EdgeTypeCensus>
What each edge type is, without building one record per edge.
all_edges_for_export answers the same
question by materialising every edge — three Strings apiece, a
provenance HashMap over every derived edge, and a final sort. That is
the right shape for an export, and the wrong one for a summary: on a
store with 1.3 M derived edges it allocates hundreds of megabytes to
produce nine lines. This walks the topology instead, summing neighbour
slice lengths and collecting label symbols rather than label strings,
so the per-edge cost is an integer add and a set insert on a set with
as many members as the store has labels.
The rule names come off the rule definitions, which each declare the
edge_type they derive, so naming them costs one pass over the rules
rather than one provenance lookup per edge. That is also why rules
is a list: two rules may derive the same type — the association store
derives INDUSTRY_ALIGNMENT from both a talent→company and a
talent→job rule — and naming only one of them would be a half-truth.
A type with no rules is one written by hand.
sample is the first edge of the type in the store’s own id order,
which is insertion order: deterministic for a given store, and not the
same as key order, which cannot be had without resolving a key per
edge. Sorted by edge_type.
Sourcepub fn weighted_edges(
&self,
edge_type: &str,
weight_prop: Option<&str>,
) -> Vec<(String, String, Option<f64>)>
pub fn weighted_edges( &self, edge_type: &str, weight_prop: Option<&str>, ) -> Vec<(String, String, Option<f64>)>
All directed edges of edge_type, with the raw value of weight_prop
on each edge when given.
weight is Some(f) only when weight_prop is set and the edge
carries that property with a numeric (Int/Float) value; otherwise
None — callers that want a default weight (e.g. 1.0 for missing
props) apply it themselves, matching the convention used internally
by GraphDb::pagerank, GraphDb::connected_components,
GraphDb::degree_centrality, and GraphDb::communities.
Sorted by (src, dst) for determinism. Reads the unified topology
(manual + rule-derived edges). An unknown edge_type returns an
empty vec.
pub fn nodes_with_label(&self, label: &str) -> Vec<NodeRef<'_, F>>
pub fn find_nodes(&self, label: &str, filter: &Filter) -> Vec<NodeRef<'_, F>>
Sourcepub fn has_vector_rule(&self, field: &str) -> bool
pub fn has_vector_rule(&self, field: &str) -> bool
Returns true if any approximate (HNSW) VectorSimilar rule covers
field. Use as a capability probe: when true, find_similar_vector
with label = None will use the native ANN path rather than the O(n)
brute-force scan.
Sourcepub fn find_similar_vector(
&self,
field: &str,
label: Option<&str>,
q: &[f64],
k: usize,
min: f64,
) -> Vec<(String, f64)>
pub fn find_similar_vector( &self, field: &str, label: Option<&str>, q: &[f64], k: usize, min: f64, ) -> Vec<(String, f64)>
Find nodes whose field vector is most similar to q (cosine
similarity), returning up to k results with similarity ≥ min,
sorted descending.
When label is None the search spans all labels (via
hnsw_search_any_dst or a full brute-force scan); when label is
Some(lbl) it restricts to nodes with that label.
Uses the HNSW index when one is available (fast path); otherwise falls back to an O(n) brute-force scan.
The index supplies candidates, never scores. Its own distances are
f32 (accurate to ~1e-6, so an exact duplicate scores 0.9999999), so
every candidate is re-scored from the f64 property vectors by
[exact_vector_similarity] before min, the ordering and the reported
score are decided. k + VECTOR_RESCORE_MARGIN candidates are fetched so
the re-ordering cannot drop a true top-k member; see that constant for
the rule. The score a caller receives is therefore the same number the
brute-force path would have produced, to f64 precision, and min = 1.0
finds an exact duplicate.
Sourcepub fn find_similar_vector_masked(
&self,
field: &str,
label: Option<&str>,
q: &[f64],
k: usize,
min: f64,
mask: &NodeMask,
) -> Vec<(String, f64)>
pub fn find_similar_vector_masked( &self, field: &str, label: Option<&str>, q: &[f64], k: usize, min: f64, mask: &NodeMask, ) -> Vec<(String, f64)>
Like [find_similar_vector] but restricts results to nodes visible in
mask. Hidden nodes never appear in results; the mask is applied
before k-truncation so a caller still receives up to k visible
hits.
§HNSW path (widening beam)
When an HNSW index covers the request, the beam starts at an over-fetch
of k × n / |visible| (plus the rescore margin) when the mask’s
selectivity is known from the index length, otherwise at k plus that
margin. If fewer than k visible candidates remain after the mask and
min filter, the beam doubles — the same ×2 loop exact VectorSimilar
rules use, capped at ef_max() (EF_MAX = 4,096). Reaching the cap,
or a beam that comes back short of its own width, falls through to the
exhaustive masked scan rather than returning a short result.
Every surviving candidate is re-scored from the f64 property vectors,
exactly as [find_similar_vector] does and for the same reason.
§Brute-force path
When no HNSW index covers the request, or the beam cannot admit k
hits, the function builds a masked GraphView so that nodes_all /
nodes_with_label return only visible nodes, guaranteeing exact k
results (or all visible nodes if fewer than k exist).
Sourcepub fn find_similar_vector_filtered(
&self,
field: &str,
label: Option<&str>,
q: &[f64],
k: usize,
min: f64,
mask: Option<&NodeMask>,
where_: Option<&PropPredicate>,
exact: bool,
) -> Result<Vec<(String, f64)>>
pub fn find_similar_vector_filtered( &self, field: &str, label: Option<&str>, q: &[f64], k: usize, min: f64, mask: Option<&NodeMask>, where_: Option<&PropPredicate>, exact: bool, ) -> Result<Vec<(String, f64)>>
Exact or ANN kNN with optional key-list mask and property where_.
where_ present and failing PropPredicate::validate_named "where"
→ QueryError. exact=true or where_=Some skip HNSW and GEMM-brute
the candidate set (label ∩ mask ∩ holds(where)). mask alone still
uses HNSW when an index covers the field.
Sourcepub fn pairwise_similar(
&self,
keys: &[&str],
field: &str,
k: usize,
min: f64,
) -> Result<Vec<(String, Vec<(String, f64)>)>>
pub fn pairwise_similar( &self, keys: &[&str], field: &str, k: usize, min: f64, ) -> Result<Vec<(String, Vec<(String, f64)>)>>
Exact cosine top-k for each key in keys, scored only against keys.
min is cosine similarity in [-1, 1], inclusive (score >= min), the
same unit and inequality as find_similar_vector. Self-matches are
excluded. Unknown keys, keys with no field, zero-norm or wrong-dim
embeddings are omitted as both query and candidate. Duplicate keys are
collapsed, first-seen order. Empty keys → empty Ok(vec![]). Never
uses HNSW. n > PAIRWISE_MAX_N → QueryError.
Sourcepub fn get_edge_prop(
&self,
edge_type: &str,
src_key: &str,
dst_key: &str,
field: &str,
) -> Option<Value>
pub fn get_edge_prop( &self, edge_type: &str, src_key: &str, dst_key: &str, field: &str, ) -> Option<Value>
Read a single property from an edge.
Returns None when the edge does not exist, the field is absent, or any
of the string keys cannot be resolved to interned ids. Only edge props
written by rules (weight fields) are accessible without a set_edge_prop
binding; topology-only edges (no props set) return None for every field.
Sourcepub fn query(
&self,
cypher: &str,
params: &BTreeMap<String, Value>,
) -> Result<ResultSet>
pub fn query( &self, cypher: &str, params: &BTreeMap<String, Value>, ) -> Result<ResultSet>
Lex → parse → plan → execute cypher over a read-only view.
Every pipeline Err(String) becomes GraphError::QueryError with a
stage prefix (lex: / parse: / plan: / execute:).
Sourcepub fn query_with_params(
&self,
cypher: &str,
params: &[(&str, Value)],
) -> Result<ResultSet>
pub fn query_with_params( &self, cypher: &str, params: &[(&str, Value)], ) -> Result<ResultSet>
Convenience entry-point that accepts a slice of (name, value) pairs
instead of a pre-built BTreeMap. Equivalent to building the map and
calling GraphDb::query.
Sourcepub fn query_write(
&mut self,
cypher: &str,
params: &BTreeMap<String, Value>,
) -> Result<ResultSet>
pub fn query_write( &mut self, cypher: &str, params: &BTreeMap<String, Value>, ) -> Result<ResultSet>
Execute a Cypher write statement (CREATE / MATCH…SET / MATCH…DELETE / MERGE).
All mutations flow through the same insert_node / set_prop /
delete_edge / insert_edge path as the Rust API so the rule engine
fires and the WAL captures everything with one fsync per statement.
Returns a one-row ResultSet with columns created, properties_set,
and deleted matching the write-result contract.
Mutation routing: mutations are collected into a single
BatchBuilder and committed atomically (one WAL Batch frame, one
fsync). The MATCH phase for SET/DELETE uses a read-only execute call
over self.view() — the borrow is dropped before the batch is opened.
Limitations (v1):
- SET RHS must be a literal,
$param, or arithmetic; bare property copy → named error. DETACH DELETE n→ callsdelete_nodefor each matched node (removes all edges).- Bare
DELETE n→ error if n has any incident edges; succeeds for isolated nodes. - MERGE supports
ON CREATE SET/ON MATCH SETin the same write batch. - Deleting a derived edge → named error “cannot delete derived edge”.
Sourcepub fn explain(&self, key_a: &str, key_b: &str) -> Result<Vec<Explanation>>
pub fn explain(&self, key_a: &str, key_b: &str) -> Result<Vec<Explanation>>
Return all rule-owned edges between key_a and key_b (either direction),
annotated with rule name, edge type, direction, and weight.
Results are sorted by (rule, edge_type).
Returns Err(KeyNotFound) if either key is unknown.
pub fn neighbors( &self, key: &str, edge_type: &str, dir: Direction, ) -> Result<Vec<String>>
Sourcepub fn degree(
&self,
key: &str,
edge_type: Option<&str>,
direction: AlgoDir,
) -> Result<u64>
pub fn degree( &self, key: &str, edge_type: Option<&str>, direction: AlgoDir, ) -> Result<u64>
Unique directed degree of key. Unknown key → GraphError::KeyNotFound.
Unknown edge_type → 0. crate::algo::AlgoDir::Both is out + in (sum).
Sourcepub fn degrees(
&self,
keys: Option<&[String]>,
label: Option<&str>,
where_: Option<&PropPredicate>,
edge_type: Option<&str>,
direction: AlgoDir,
limit: Option<usize>,
) -> Result<Vec<(String, u64)>>
pub fn degrees( &self, keys: Option<&[String]>, label: Option<&str>, where_: Option<&PropPredicate>, edge_type: Option<&str>, direction: AlgoDir, limit: Option<usize>, ) -> Result<Vec<(String, u64)>>
Unique directed degree for a subset or a label scan.
Unknown keys in keys are omitted (mask-like). keys = Some(&[]) →
empty Ok(vec![]). limit is applied after sorting degree desc, key
asc, and only when Some. Invalid where_ → QueryError.
Sourcepub fn last_changed(&self, key: &str) -> Option<u64>
pub fn last_changed(&self, key: &str) -> Option<u64>
Return the last-change commit sequence for key, or None if the node
does not exist or has never been mutated since the last V5-V7 snapshot
(horizon-bounded for legacy stores).
The returned sequence is a monotonically increasing counter that starts
at 1 for the first commit after open and increments with every
successful write. WAL replay at open also assigns sequences (1..N for N
replayed frames), so sequences are consistent across snapshot+WAL cycles.
For V5-V7 stores opened without a V8 snapshot, nodes that were present
in the snapshot but not touched by any WAL frame will return None
(horizon-bounded: CAS against such nodes is only safe after the first
V8 snapshot or after the node is next mutated).
Sourcepub fn commit_seq(&self) -> u64
pub fn commit_seq(&self) -> u64
The current commit sequence (number of successful commits since open, including WAL replay frames). Useful for recording a baseline before a read-modify-write cycle.
Sourcepub fn write_batch_cas(
&mut self,
preconds: Vec<Precondition>,
ops: Vec<BatchOp>,
) -> Result<(usize, usize)>
pub fn write_batch_cas( &mut self, preconds: Vec<Precondition>, ops: Vec<BatchOp>, ) -> Result<(usize, usize)>
Apply a batch of mutations with compare-and-set preconditions.
All preconditions are checked atomically before any operation is applied.
If any precondition fails, the entire batch is rejected with
GraphError::CasConflict and no WAL frame is written.
§Returns
(nodes_inserted, edges_inserted) on success, same as [write_batch].
§Errors
GraphError::CasConflictif any precondition is not satisfied.- Any error that [
write_batch] would return for the ops themselves.
pub fn node_count(&self) -> usize
Sourcepub fn set_wal_archive_retention(&mut self, keep: Option<u32>)
pub fn set_wal_archive_retention(&mut self, keep: Option<u32>)
Configure archive retention: keep the N newest WAL archives at each
[snapshot_with] call when archive_wal: true.
Some(N) where N > 0 → prune oldest archives keeping the newest N.
Some(0) or None → unlimited (no pruning).
Pruning only ever happens inside [snapshot_with]; this method only
stores the policy. Archives below the retention limit are deleted
oldest-first. The horizon floor is updated so that
[was_linked] / history APIs return CommitOutOfRange for commits
in pruned archives rather than silently returning wrong data.
Sourcepub fn wal_total_commits(&self) -> Result<u64>
pub fn wal_total_commits(&self) -> Result<u64>
Return the total number of committed WAL frames visible in the current horizon window, including frames in surviving WAL archives.
This is the exclusive upper bound for valid at_commit indices in
was_linked. Valid indices are wal_horizon_floor()..wal_total_commits().
Returns the horizon floor when all surviving history is empty.
Sourcepub fn wal_horizon_floor(&self) -> u64
pub fn wal_horizon_floor(&self) -> u64
The global frame index of the first commit reachable through surviving archives (0 when no archives have been pruned).
Sourcepub fn node_history(&self, key: &str) -> Result<HistoryResult<HistoryEntry>>
pub fn node_history(&self, key: &str) -> Result<HistoryResult<HistoryEntry>>
Return the change history of node key by scanning the on-disk WAL.
§Horizon
History reaches back only as far as the retained WAL. The returned
HistoryResult carries total_commits
(the exclusive upper bound for valid commit indices) and horizon (the
oldest commit still reachable). When horizon > 0, older events were
pruned and are not in items.
Sourcepub fn edge_history(
&self,
a: &str,
b: &str,
) -> Result<HistoryResult<EdgeHistoryEvent>>
pub fn edge_history( &self, a: &str, b: &str, ) -> Result<HistoryResult<EdgeHistoryEvent>>
Return the per-edge change history between nodes a and b by scanning
the on-disk WAL.
§Horizon
History reaches back only to the last WAL-truncating snapshot, exactly
like node_history and open_at. The returned [HistoryResult] carries
total_commits (= number of WAL frames), which is the exclusive upper
bound for valid commit indices.
§Derived edges
Rule-derived edges appear via DerivedEdgeAdded / DerivedEdgeRetracted
WAL markers written by log_then_apply_with after each rule-firing
mutation. The rule field of those events carries the rule name.
§DeleteNode
When a node is deleted, its manual incident edges are swept inline without
individual DeleteEdge WAL records. edge_history detects DeleteNode
events for either endpoint and synthesises Retracted(rule:None) events
for each manual edge that was active at that point. Derived edges active at
the time of deletion are handled by the DerivedEdgeRetracted marker that
the engine appends immediately after the DeleteNode record; those events
carry correct rule attribution and are emitted by the marker arm, not the
synthetic sweep.
§Masks
Like node_history, this method has no mask parameter and returns WAL
history regardless of any role mask. For masked history semantics, apply
the mask at the caller level.
Sourcepub fn was_linked(
&self,
a: &str,
b: &str,
edge_type: &str,
at_commit: u64,
) -> Result<bool>
pub fn was_linked( &self, a: &str, b: &str, edge_type: &str, at_commit: u64, ) -> Result<bool>
Return true iff an edge of edge_type existed between a and b
(in either direction) at the WAL commit at_commit.
§Horizon
Valid commit indices are 0..total_commits where total_commits is the
number of WAL frames. An at_commit >= total_commits is outside the
visible horizon and returns GraphError::CommitOutOfRange.
§Derived edges
Rule-derived edges are tracked via DerivedEdgeAdded / DerivedEdgeRetracted
WAL markers appended at firing time (Task 1). was_linked reads these markers
and therefore includes derived edges in its point-in-time evaluation,
matching edge_history’s fidelity.
Sourcepub fn edges_at(&self, key: &str, commit: u64) -> Result<Vec<EdgeAt>>
pub fn edges_at(&self, key: &str, commit: u64) -> Result<Vec<EdgeAt>>
Every edge incident to key — either endpoint — that existed at WAL
commit commit, from ONE scan of the WAL.
This is the bulk form of was_linked: answering
“what did K’s relationships look like at commit C” with one call instead
of one edge_history per candidate partner.
The two agree edge for edge.
Results are sorted by (edge_type, src_key, dst_key).
§Horizon
Valid commit indices are wal_horizon_floor()..wal_total_commits();
anything outside is GraphError::CommitOutOfRange, exactly like
was_linked. An unknown key is not an error — it simply had no edges.
§Derived edges
DerivedEdgeAdded / DerivedEdgeRetracted markers carry rule
attribution, so a rule-owned edge comes back with derived: true and
rule: Some(name).
§Renames
key is matched through the same commit-bounded alias intervals
edge_history uses, so querying a node’s current key surfaces edges
written under an earlier name. Endpoint keys in the result are reported
under the name the node carries today, so they can be fed straight back
into node_info, explain or another edges_at.
§Masks
Like edge_history and node_history, this reads the WAL regardless of
any role mask. Apply masking at the caller level.
Sourcepub fn what_if_set_prop(
&self,
key: &str,
field: &str,
value: Value,
) -> Result<WhatIf>
pub fn what_if_set_prop( &self, key: &str, field: &str, value: Value, ) -> Result<WhatIf>
The derived edges that would be retracted and derived if key.field
were set to value — computed WITHOUT writing anything.
Nothing is committed and nothing on self is mutated: the rule engine’s
provenance, its candidate indexes, the topology and the property columns
are all cloned first, the change is applied to the clone, and the real
per-node re-derivation (RuleEngine::on_node_changed — the same call
set_prop makes during apply) runs against it. The derived-edge deltas
it emits are the answer, so rule semantics — predicates, top-k,
via-hops, chaining, weights — are the engine’s, not a re-implementation.
Works on a read-only handle.
While a rule’s vector index is still building (RuleStats::building)
the clone carries no pending-build state, so this reports the edges that
rule would derive — which the live store will not derive until its
backfill runs. Right about the end state, early about the timing.
Returns Err(KeyNotFound) for an unknown or tombstoned key and
Err(ViewPropReadOnly) for a field a view owns — matching
set_prop’s validation. A change with no effect
(the node already holds value, or no rule watches field) returns
empty lists.
§Cost
One clone of the property columns, the topology overlay, the symbol interner, the edge properties and the provenance map, plus one candidate re-index (O(nodes × rules)). That is much cheaper than copying the store directory, but it is not free — this is an interactive “what if”, not a hot path.
pub fn edge_count(&self) -> u64
Sourcepub fn stats(&self) -> Stats
pub fn stats(&self) -> Stats
Live/tombstone/edge counts plus per-rule provenance size, trip latch, and fire counter (includes rebuild evaluations). Rules are sorted by name.
Sourcepub fn wal_size_bytes(&self) -> Result<u64>
pub fn wal_size_bytes(&self) -> Result<u64>
On-disk size of the WAL file in bytes.
Reads file metadata without loading WAL contents. Returns Err for
in-memory (SimFs) databases where no WAL file exists on disk.
Sourcepub fn set_slow_query_threshold_ms(&mut self, ms: u64)
pub fn set_slow_query_threshold_ms(&mut self, ms: u64)
Set the slow-query threshold. Queries whose execution time equals or
exceeds ms milliseconds are logged. Pass 0 to disable.
Use this setter in tests — the environment variable
MUSHROOMDB_SLOW_QUERY_MS is process-global and races parallel test
threads.
Sourcepub fn slow_query_snapshot(&self) -> SlowQuerySnapshot
pub fn slow_query_snapshot(&self) -> SlowQuerySnapshot
Snapshot of the slow-query ring buffer and lifetime counter.
Sourcepub fn started_at(&self) -> Instant
pub fn started_at(&self) -> Instant
Instant the database was opened. Used by consumers (e.g. /metrics)
to compute uptime.
Sourcepub fn fs_total_appended(&self) -> usizewhere
F: FsIntrospect,
pub fn fs_total_appended(&self) -> usizewhere
F: FsIntrospect,
Test-support: total bytes appended (SimFs only usage).
Sourcepub fn fs_sync_count(&self) -> usizewhere
F: FsIntrospect,
pub fn fs_sync_count(&self) -> usizewhere
F: FsIntrospect,
Test-support: successful Fs::sync calls (SimFs / counting fs).
pub fn snapshot(&mut self) -> Result<()>
Sourcepub fn snapshot_with(&mut self, opts: SnapshotOptions) -> Result<()>
pub fn snapshot_with(&mut self, opts: SnapshotOptions) -> Result<()>
Snapshot with explicit options.
§keep_wal
When keep_wal is false (the default, same as [snapshot]):
- The WAL is replaced with a minimal baseline containing one
EnableFulltextrecord per active declaration. All pre-snapshot history is discarded;open_atcan only reach post-snapshot commits.
When keep_wal is true:
- The WAL is left intact. All pre-snapshot commits remain reachable
via
open_at. The existing WAL already contains the originalEnableFulltextrecords, so no baseline re-write is needed; the recovery guards inapply()silently skip any duplicate records on replay. - Crash window: a crash after the snapshot write but before the next
WAL write leaves the full pre-snapshot WAL intact. On reopen the
snapshot is loaded and the WAL replayed idempotently over it — safe
because every
apply()arm is idempotent when replayed over an already-current snapshot.
Sourcepub fn apply_schema(&mut self, schema: &Schema) -> Result<SchemaDiff>
pub fn apply_schema(&mut self, schema: &Schema) -> Result<SchemaDiff>
Apply schema to the database idempotently.
Returns a SchemaDiff describing what was created, updated, or left
unchanged. The diff is in application order: fulltext, then views,
then rules.
Items absent from schema but present in the database are left
untouched (no pruning).
§Atomicity of validation
All rules and views that would be created or updated are validated
before any mutation is made. If any definition is invalid, the
function returns Err without touching the database. This prevents
the partial-application hazard where the old item is already deleted
before the invalid replacement fails.
§Update cost
Updating a rule (definition differs) triggers delete_rule +
create_rule. The create_rule call runs a full backfill of derived
edges. This can be expensive for large graphs; prefer stable rule
definitions in production.
Trait Implementations§
Source§impl<'a> Deref for WriteGuard<'a>
impl<'a> Deref for WriteGuard<'a>
Source§impl<'a> DerefMut for WriteGuard<'a>
impl<'a> DerefMut for WriteGuard<'a>
Source§impl<'a> Drop for WriteGuard<'a>
impl<'a> Drop for WriteGuard<'a>
Auto Trait Implementations§
impl<'a> !Send for WriteGuard<'a>
impl<'a> Freeze for WriteGuard<'a>
impl<'a> RefUnwindSafe for WriteGuard<'a>
impl<'a> Sync for WriteGuard<'a>
impl<'a> Unpin for WriteGuard<'a>
impl<'a> UnsafeUnpin for WriteGuard<'a>
impl<'a> UnwindSafe for WriteGuard<'a>
Blanket Implementations§
Source§impl<T> ArchivePointee for T
impl<T> ArchivePointee for T
Source§type ArchivedMetadata = ()
type ArchivedMetadata = ()
Source§fn pointer_metadata(
_: &<T as ArchivePointee>::ArchivedMetadata,
) -> <T as Pointee>::Metadata
fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata
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> LayoutRaw for T
impl<T> LayoutRaw for T
Source§fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
Source§impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
Source§unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
Source§fn resolve_niched(out: Place<NichedOption<T, N1>>)
fn resolve_niched(out: Place<NichedOption<T, N1>>)
out indicating that a T is niched.