pub struct GraphDb<F: Fs> { /* private fields */ }Implementations§
Source§impl GraphDb<RealFs>
impl GraphDb<RealFs>
pub fn open(dir: &Path) -> Result<Self>
Sourcepub fn open_at(dir: &Path, commit: u64) -> Result<Self>
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
GraphError::CommitOutOfRangeifcommit >= wal_commit_count(including when the WAL is empty after a snapshot).
Source§impl<F: Fs> GraphDb<F>
impl<F: Fs> GraphDb<F>
pub fn open_with(fs: F) -> Result<Self>
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 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 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.
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 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 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 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 search(&self, field: &str, query: &str) -> Vec<(String, usize)>
pub fn search(&self, field: &str, query: &str) -> Vec<(String, usize)>
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.
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 and no HNSW rule covers vector_field, the
brute-force scan cannot enumerate a node universe; find_similar_vector
returns an empty result and the fused ranking is text-only. Document
this in your application layer if you rely on it.
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 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.
pub fn get_prop(&self, key: &str, field: &str) -> Option<&Value>
pub fn has_node(&self, key: &str) -> bool
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 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_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.
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 find_similar_vector(
&self,
field: &str,
label: &str,
q: &[f64],
k: usize,
min: f64,
) -> Vec<(String, f64)>
pub fn find_similar_vector( &self, field: &str, label: &str, q: &[f64], k: usize, min: f64, ) -> Vec<(String, f64)>
Find nodes with the given label whose field vector is most similar
to q (cosine similarity), returning up to k results with similarity
≥ min, sorted descending.
Uses the HNSW index when one is available (fast path); otherwise falls back to an O(n) brute-force scan over all nodes with that label (exact).
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>>
pub fn node_count(&self) -> usize
Sourcepub fn node_history(&self, key: &str) -> Result<Vec<HistoryEntry>>
pub fn node_history(&self, key: &str) -> Result<Vec<HistoryEntry>>
Return the per-node change history for key by scanning the on-disk WAL.
§Horizon
History reaches back only to the last WAL-truncating snapshot, exactly like open_at.
Snapshots written with keep_wal: true preserve deeper history. This is the honest,
zero-cost contract; a durable history log is out of scope.
§Derived edges
Rule-created (derived) edges are not in the WAL and therefore do not appear in history. Only edges written directly by the application are recorded.
§Deleted nodes
For nodes that have been deleted, dense-id records (SetPropId, InsertEdgeId) that
predate the deletion may not resolve (the id is tombstoned in the live map). The
string-keyed DeleteNode record still matches and produces a NodeDeleted entry.
Prop/edge history of a deleted node may therefore be partially unresolvable.
§Dense-id edge entries and tombstoned partners
Edge entries from dense-id WAL records (InsertEdgeId) are omitted when the partner
endpoint’s dense id is tombstoned. As a result, a live node’s history can contain an
EdgeRemoved (string-keyed, always resolves) without a corresponding EdgeAdded.
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 format_version() -> u16
pub fn format_version() -> u16
On-disk snapshot format version this binary writes and reads.
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.
Source§impl<F: Fs> GraphDb<F>
impl<F: Fs> GraphDb<F>
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.