<!-- nornir:class: guide -->
# Using `knut-graphdb`
> **Class:** `guide`. How to read a graph endpoint, and how to write a backend.
> **Source:** `crates/knut-graphdb/`.
## Read a FalkorDB
```rust
use knut_graphdb::{FalkorBackend, GraphDb};
let db = FalkorBackend::new("127.0.0.1:6379"); // or "redis://…" / "rediss://…"
for name in db.graph_names().await? { // every graph on the endpoint
let schema = db.open(&name).await?; // labels, rel types, keys, counts
let scene = db.read(&name, 300).await?; // nodes + edges, capped
println!("{name}: {} labels, {} nodes", schema.labels.len(), scene.nodes.len());
}
```
`FalkorBackend::new` takes an **endpoint and nothing else**. A bare `host:port` is
normalised to `redis://host:port`; a string that already carries a scheme is passed
through untouched, so a `rediss://` endpoint is never downgraded to plaintext.
Two knobs, both optional:
```rust
let db = FalkorBackend::new("rediss://falkor.internal:6379")
.with_identity(tls) // mutual-TLS client identity
.with_timeout(Duration::from_secs(5)); // per-op bound
```
The identity is a **credential, not a lifecycle knob** — it changes how the backend
dials, never whether the endpoint exists. Its bytes are never logged: `Debug` prints
`mtls: true` and nothing else.
The default bound is `KORP_GRAPH_READ_TIMEOUT_MS` or 30 s, and it can never be zero —
a parse to 0 falls back to the default, so the bound always fires.
## Read an embedded Cozo (no container)
```rust
use knut_graphdb::{CozoBackend, GraphDb};
let db = CozoBackend::shared()?; // process-global, demo-seeded once
assert_eq!(db.graph_names().await?, vec!["korp".to_string()]);
let scene = db.read("korp", 0).await?; // 0 ⇒ DEFAULT_SCENE_LIMIT
```
Cozo speaks **Datalog**, not Cypher. It does not implement `Cypher` at all, so there
is nothing to stub and nothing to panic from. Ask it for a query bar and you get a
value, not a crash:
```rust
if !db.cypher_support().is_available() {
let refusal = knut_graphdb::no_cypher_here(&db); // GraphError::NoQueryLanguage
}
```
## Run Cypher (only where it exists)
```rust
use knut_graphdb::{Cypher, FalkorBackend};
let db = FalkorBackend::new("127.0.0.1:6379");
let sub = db.cypher("social", "MATCH (a)-[r]->(b) RETURN …", 300).await?;
```
`limit` is passed to `ensure_limit`, so a query that bounds itself keeps its own cap
and an unbounded one gets `LIMIT <limit>`. **`limit = 0` means "the caller already
bounded this"** and the Cypher reaches the engine verbatim. An empty query falls back
to the Person-anchored `scene_cypher`.
## Telling the four answers apart
```rust
match db.graph_names().await {
Ok(names) if names.is_empty() => …, // REACHABLE and empty
Ok(names) => …,
Err(GraphError::Connect(_)) => …, // unreachable
Err(GraphError::Timeout { .. }) => …, // past the bound
Err(e) => …,
}
```
`db.open("typo")` is `GraphError::NotFound`, never an empty `GraphSchema` — and the
refused open leaves **no stray graph** on the endpoint, because `open` lists before it
probes.
## Write a backend
Implement five methods:
```rust
#[async_trait]
impl GraphDb for MyStore {
fn kind(&self) -> &'static str { "mystore" }
fn endpoint(&self) -> &str { &self.url }
async fn graph_names(&self) -> Result<Vec<String>, GraphError> { … }
async fn open(&self, graph: &str) -> Result<GraphSchema, GraphError> { … }
async fn read(&self, graph: &str, limit: usize) -> Result<SubGraph, GraphError> { … }
}
```
That is a complete backend. Three rules:
1. **Never return `Ok(vec![])` for an unreachable endpoint.** Empty and unreachable
are different answers and callers branch on the difference.
2. **`open` establishes existence from `graph_names` BEFORE it probes**, and a name
that is not there is `GraphError::NotFound`. If your engine creates a key on first
touch (FalkorDB does), the other order silently makes every name valid.
3. **Do not implement `Cypher` unless you speak it.** Leaving it out is the honest
answer and costs you nothing — `cypher_support()` already says `None`.
Add `neighbours` / `shortest_path` / `upsert` if you can serve them; the defaults
refuse with `GraphError::Unsupported` naming your backend and the op.
## Extras that are not on the trait
`FalkorBackend::delete_graph(graph)` (`GRAPH.DELETE`) and
`FalkorBackend::config_get_int(key)` (`GRAPH.CONFIG GET`) are **inherent** methods:
they are FalkorDB server operations, not something every graph backend has. They live
here so a consumer never opens its own redis connection to send one — which is
exactly how korp ended up with three.
## Running the live gates
```bash
podman run -d --name falkordb -p 6379:6379 falkordb/falkordb:latest
KNUT_FALKOR_ENDPOINT=127.0.0.1:6379 \
cargo test -p knut-graphdb --test live_falkordb -- --ignored --nocapture
```
Every graph they create is namespaced by pid and deleted afterwards, so they run
beside another lane's data on a shared endpoint.