Skip to main content

GraphDb

Struct GraphDb 

Source
pub struct GraphDb<F: Fs> { /* private fields */ }

Implementations§

Source§

impl GraphDb<RealFs>

Source

pub fn open(dir: &Path) -> Result<Self>

Open the database at dir with default options.

Equivalent to open_with_options(dir, OpenOptions::default()). Old-format snapshots (V5, V6) are automatically migrated to the current version on a successful load (see OpenOptions::auto_migrate).

Source

pub fn open_with_options(dir: &Path, opts: OpenOptions) -> Result<Self>

Open the database at dir with explicit options.

When opts.auto_migrate is true (the default) and the on-disk snapshot is an older format version, this function:

  1. Copies the current snapshot.bin to snapshot.bin.bak (atomic
    • fsynced) before any modification.
  2. Rewrites snapshot.bin at the current format version via GraphDb::snapshot_with with keep_wal: true (WAL preserved).

If migration fails the error is returned and the original files are intact (the .bak was written before the new snapshot was attempted).

A clean open that finds the snapshot already at the current version deletes any leftover .bak file.

WAL-only stores (no snapshot) are never auto-migrated on open.

Source

pub fn open_at(dir: &Path, commit: u64) -> Result<Self>

Open a read-only view of the database as it existed after commit.

Commit indices are 0-based over the current WAL: commit 0 is the state after the first WAL frame, commit N-1 is the state after the N-th (most recent) frame. Call GraphDb::open to read the full current state.

Replay base. GraphDb::snapshot truncates the WAL when it runs, so as-of can only reach commits recorded in the current WAL (those written after the most recent snapshot, or all commits if no snapshot was ever taken). Commit 0 in open_at always refers to the first frame in the WAL that exists on disk, not the first ever write to the database. When the on-disk snapshot recorded that it truncated the WAL (V7, default keep_wal: false), it is loaded as the base state before frame replay, so the as-of view includes all pre-snapshot data. Snapshots written with keep_wal: true (and legacy V5/V6 snapshots) are ignored and replay is WAL-only, as before.

Read-only. Every mutation method and snapshot() on the returned instance returns GraphError::ReadOnly. Queries, explain(), and stats() work normally.

§Errors
Source

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
Source§

impl<F: Fs> GraphDb<F>

Source

pub fn open_with(fs: F) -> Result<Self>

Source

pub fn is_read_only(&self) -> bool

Whether this instance is a read-only as-of view.

Source

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.

Source

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.

Source

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>>>.

Source

pub fn has_event_sink(&self) -> bool

Whether a post-commit event sink is currently installed.

Source

pub fn set_fsync_policy(&mut self, p: FsyncPolicy)

Set WAL fsync cadence. Default FsyncPolicy::Strict.

Source

pub fn fsync_policy(&self) -> FsyncPolicy

Return the current WAL fsync cadence.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.
Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn insert_node( &mut self, label: &str, key: &str, props: Vec<(String, Value)>, ) -> Result<()>

Source

pub fn insert_edge( &mut self, edge_type: &str, src_key: &str, dst_key: &str, ) -> Result<bool>

Source

pub fn set_prop(&mut self, key: &str, field: &str, value: Value) -> Result<()>

Source

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).

Source

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).

Source

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).

Source

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.

Source

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).

Source

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.

Source

pub fn delete_rule(&mut self, name: &str) -> Result<()>

WAL-log rule deletion. Returns RuleNotFound if the rule does not exist.

Source

pub fn rules(&self) -> Vec<RuleDef>

Return a snapshot of all registered rules.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.
Source

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.
Source

pub fn views(&self) -> Vec<ViewDef>

Snapshot of all registered view definitions.

Source

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
Source

pub fn disable_fulltext(&mut self, label: &str, field: &str) -> Result<()>

Disable full-text indexing for (label, field) and drop its postings.

§Errors
Source

pub fn is_fulltext_enabled(&self, label: &str, field: &str) -> bool

Whether (label, field) is currently indexed for full-text search.

Source

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
Source

pub fn disable_index(&mut self, label: &str, field: &str) -> Result<()>

Disable the equality index for (label, field) and drop its postings.

§Errors
Source

pub fn is_index_enabled(&self, label: &str, field: &str) -> bool

Whether (label, field) currently has an equality index.

Source

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.
  • OR between terms forms disjunction: "foo OR bar" matches either.
  • Trailing * on a term is a prefix match: "rust*" matches rustlang, rusty.
  • AND keyword 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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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).

Source

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
Source

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).

Source

pub fn has_node(&self, key: &str) -> bool

Source

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.json was present but corrupt at open (poisoned state), or
  • role does 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. Label resolution is live — new nodes of an allowed label are visible without re-applying the schema. An empty union = empty mask = sees nothing.

Source

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).

Source

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 to write_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.

Source

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”.

Source

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_ingestcommit_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).

Source

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).

Source

pub fn node_ref(&self, key: &str) -> Option<NodeRef<'_, F>>

Source

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.

Source

pub fn node_info(&self, key: &str) -> Option<NodeInfo>

Live node’s key, label, and columnar props. Unknown or tombstoned → None.

Source

pub fn node_info_masked( &self, key: &str, mask: &NodeMask, ) -> Option<MaskedNodeResult>

Look up a node with mask awareness.

Key stateOmit modeStub mode
does not existNone (→ 404)None (→ 404)
exists, visibleSome(Visible)Some(Visible)
exists, hiddenNone (→ 404)Some(Restricted)

SECURITY: only call from client-mask (full-token) paths. Role-token paths must use [node_info] after an explicit visibility check.

Source

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_restricted is true for each hidden endpoint.

Unknown key → GraphError::KeyNotFound.

SECURITY: only call from client-mask (full-token) paths.

Source

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.

Source

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).

Source

pub fn all_nodes_for_export(&self) -> Vec<NodeInfo>

All live nodes, sorted by key (deterministic).

Reads base + WAL overlay. Tombstoned nodes are excluded.

Source

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. Deterministic across runs on the same store state.

Source

pub fn nodes_with_label(&self, label: &str) -> Vec<NodeRef<'_, F>>

Source

pub fn find_nodes(&self, label: &str, filter: &Filter) -> Vec<NodeRef<'_, F>>

Source

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.

Source

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.

Source

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 (over-fetch policy)

When an HNSW index covers the request, this function fetches 4 * k candidates from the index and discards hidden nodes in the post-filter step. If fewer than k visible nodes remain after filtering the caller receives whatever is available — we do not re-query the index. The 4× multiplier is a heuristic suited for sparsely masked graphs; callers operating under a very selective mask should register a VectorSimilar rule with a non-approximate index, or use the brute-force path (no HNSW rule) which exhaustively filters through the masked GraphView.

§Brute-force path

When no HNSW index covers the request 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).

Source

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.

Source

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:).

Source

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.

Source

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 → calls delete_node for 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 SET in the same write batch.
  • Deleting a derived edge → named error “cannot delete derived edge”.
Source

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.

Source

pub fn neighbors( &self, key: &str, edge_type: &str, dir: Direction, ) -> Result<Vec<String>>

Source

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).

Source

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.

Source

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::CasConflict if any precondition is not satisfied.
  • Any error that [write_batch] would return for the ops themselves.
Source

pub fn node_count(&self) -> usize

Source

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.

Source

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.

Source

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).

Source

pub fn node_history(&self, key: &str) -> Result<Vec<HistoryEntry>>

Source

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.

Source

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.

Source

pub fn edge_count(&self) -> u64

Source

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.

Source

pub fn format_version() -> u16

On-disk snapshot format version this binary writes and reads.

Source

pub fn fs_total_appended(&self) -> usize
where F: FsIntrospect,

Test-support: total bytes appended (SimFs only usage).

Source

pub fn fs_sync_count(&self) -> usize
where F: FsIntrospect,

Test-support: successful Fs::sync calls (SimFs / counting fs).

Source

pub fn into_fs(self) -> F

Consume the db, returning its fs (for crash simulation).

Source

pub fn snapshot(&mut self) -> Result<()>

Source

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 EnableFulltext record per active declaration. All pre-snapshot history is discarded; open_at can 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 original EnableFulltext records, so no baseline re-write is needed; the recovery guards in apply() 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.
Source§

impl<F: Fs> GraphDb<F>

Source

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.

Auto Trait Implementations§

§

impl<F> !Freeze for GraphDb<F>

§

impl<F> !RefUnwindSafe for GraphDb<F>

§

impl<F> !UnwindSafe for GraphDb<F>

§

impl<F> Send for GraphDb<F>
where F: Send,

§

impl<F> Sync for GraphDb<F>
where F: Sync,

§

impl<F> Unpin for GraphDb<F>
where F: Unpin,

§

impl<F> UnsafeUnpin for GraphDb<F>
where F: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> ArchivePointee for T

Source§

type ArchivedMetadata = ()

The archived version of the pointer metadata for this type.
Source§

fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata

Converts some archived metadata to the pointer metadata for itself.
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> LayoutRaw for T

Source§

fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>

Returns the layout of the type.
Source§

impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
where T: SharedNiching<N1, N2>, N1: Niching<T>, N2: Niching<T>,

Source§

unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool

Returns whether the given value has been niched. Read more
Source§

fn resolve_niched(out: Place<NichedOption<T, N1>>)

Writes data to out indicating that a T is niched.
Source§

impl<T> Pointee for T

Source§

type Metadata = ()

The metadata type for pointers and references to this type.
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.