<!-- ⚠ GENERATED by nornir from .nornir/README.md — DO NOT EDIT this file -->
<p align="center"><img src="assets/generated_knut_iceberg.png" alt="knut — a knytpunkt over graphs" width="560"></p>
# knut
> A pure-Rust **columnar ↔ graph fabric** — read/write Apache **GraphAr**, bulk-load
> **FalkorDB**, and serve **Cypher over Arrow Flight SQL**, with **Bifröst** streaming
> **Iceberg** tables into a live graph by CDC (Spark-Connect *or* Spark-free). One Arrow
> pipe from the lakehouse to a queryable graph.
⚡ part of the **nordisk** constellation
## Mashup — one Arrow pipe: columnar lake → live graph
knut carries graph data the whole way from a columnar lake to a live, queryable graph in
**one Arrow pipe** (no JVM required) — the rows marked **NA** are categories the rivals have
no concept for:
| capability | **knut** | GraphAr (official) | Neo4j / GDS | Spark + GraphFrames |
|---|:--:|:--:|:--:|:--:|
| Pure-**Rust** GraphAr — 5 formats (Parquet/CSV/ORC/JSON/Arrow), local **or** object store | **✅** | ◐ (C++/Java/Py) | ✗ | ◐ |
| **Cypher over Arrow Flight SQL** server — ADBC / Power BI / DataFusion clients, Arrow out | **✅** | **NA** | ◐ | ✗ |
| **Iceberg → graph CDC** fabric — snapshot-incremental + changelog (`_change_type` → MERGE / DETACH DELETE) | **✅** | **NA** | ✗ | ◐ |
| **Spark-free** Iceberg ingestion (skade `IcebergSource`, no JVM) | **✅** | **NA** | ✗ | ✗ |
| Bulk columnar `RecordBatch` → **live graph DB** (FalkorDB UNWIND/CREATE) | **✅** | ◐ | ✅ | ✅ |
| Mature graph-algorithm library + production graph engine | ◐ | ✗ | **✅** | **✅** |
*Rows marked **NA**: the rival has no such category. The measured numbers are below.*
## Benchmarks
<!-- nornir:gen:start:bench_status -->
**v0.1.3 · oden · 32 cores · 2026-07-23**
| | |
|---|---|
| host | `oden` · 32 cores |
| version | `0.1.3` |
| measured | 2026-07-23 |
| arms | 29 |
| borrowed arms | **10** named `nornir.*` — measured here but owned by `nornir` |
| tests | 29/29 passed |
| publishable | yes |
| flagged implausible | **2** |
⚠ **2 arms carrying a number that is not credible as real work.** Each is marked ⚠ or withheld wherever it appears below — never published as a measurement:
| arm | what is not credible |
|---|---|
| `bifrost_fabric` | `rows_per_s` = 6.698e9/s derived from a 1.49e-5 s interval — under the 1e-4 s timing floor, so the rate is arithmetic, not measurement |
| `merge_property_groups` | `rows_per_s` = 1.067e10/s derived from a 9.37e-6 s interval — under the 1e-4 s timing floor, so the rate is arithmetic, not measurement |
ℹ **10 arms in this run belong to another repo** (10 × `nornir.*`). They are the harness's own suite, pulled in because the bench runner this repo pins predates the arm filter that would have excluded them — so they were measured here without being asked for. They describe `nornir`, not `knut`, and are shown separately below rather than mixed into this repo's numbers.
<!-- nornir:gen:end:bench_status -->
### The Arrow pipe — knut's own arms
Storage formats round-tripped through the GraphAr writer/reader, the Bifröst CDC
fabric, the FalkorDB bulk path, and the Thund streaming window operators.
<!-- nornir:gen:start:benches bold=false cols=rows_per_s,read_rows_per_s,write_rows_per_s,incremental_rows_per_s,recompute_rows_per_s,checkpointed_rows_per_s,restore_resume_rows_per_s,speedup_x,telem_cores_busy_avg,telem_elapsed_ms exclude=nornir. suspect=exclude -->
**v0.1.3 · oden · 32 cores · 2026-07-23**
| workload | rows_per_s | read_rows_per_s | write_rows_per_s | incremental_rows_per_s | recompute_rows_per_s | checkpointed_rows_per_s | restore_resume_rows_per_s | speedup_x | telem_cores_busy_avg | telem_elapsed_ms |
|---|---|---|---|---|---|---|---|---|---|---|
| builder_dump | 1.42M | | | | | | | | 1 | 71.43 |
| cypher_render | 3.21M | | | | | | | | 1 | 38.22 |
| doput_dispatch | | | | | | | | 0.85 | 1.60 | 4766 |
| falkor_parallel_dispatch | | | | | | | | 29.75 | 1 | 801.77 |
| file_source_scan | 48.55M | | | | | | | | 1 | 20.62 |
| format_arrow | | 170.32M | 77.14M | | | | | | 1 | 10.89 |
| format_csv | | 8.18M | 5.44M | | | | | | 2 | 42.19 |
| format_json | | 1.08M | 8.44M | | | | | | 2 | 113.17 |
| format_orc | | 63.84M | 47.38M | | | | | | 1 | 12.46 |
| format_parquet | | 45.57M | 7.16M | | | | | | 2 | 24.88 |
| graphar_vertex_load | 35.85M | | | | | | | | 2 | 54.49 |
| ingest_lifecycle | 36.05M | | | | | | | | 2 | 24.69 |
| thund_checkpoint | | | | | | 14.11M | | | 2 | 157.51 |
| thund_incremental_agg | | | | 6.13M | 1.80M | | | | 1.50 | 1467 |
| thund_recovery | | | | | | | 33.59M | | 1 | 129.08 |
| thund_watermark_drop | | | | 2.22M | 3.81M | | | | 1 | 36.46 |
| thund_window_close | | | | 16.58M | 31.40M | | | | 1 | 193.79 |
⚠ **withheld from this table — not credible as real work:**
- `bifrost_fabric` — `rows_per_s` = 6.698e9/s derived from a 1.49e-5 s interval — under the 1e-4 s timing floor, so the rate is arithmetic, not measurement
- `merge_property_groups` — `rows_per_s` = 1.067e10/s derived from a 9.37e-6 s interval — under the 1e-4 s timing floor, so the rate is arithmetic, not measurement
<!-- nornir:gen:end:benches -->
<sub>`telem_cores_busy_avg` is the average number of CPU cores busy while the arm ran —
a single-digit value on a 32-core host means the arm is serial by construction, not that
the box was idle. Blank cells are metrics that arm does not report.</sub>
### Borrowed arms — nornir's own suite, measured here by accident
<!-- nornir:gen:start:benches bold=false cols=elapsed_ms,rows_per_sec,queries_per_sec,symbols_per_sec,events_per_sec,speedup_vs_full,telem_cores_busy_avg name=nornir. suspect=exclude -->
**v0.1.3 · oden · 32 cores · 2026-07-23**
| workload | elapsed_ms | rows_per_sec | queries_per_sec | symbols_per_sec | events_per_sec | speedup_vs_full | telem_cores_busy_avg |
|---|---|---|---|---|---|---|---|
| nornir.delta_rescan | 8.19 | | | | | 2.70 | 0 |
| nornir.dep_graph_build | 24.07 | | 365085 | | | | 1 |
| nornir.dep_graph_build_ext | 31.88 | | 344637 | | | | 1 |
| nornir.funnel_append | 1284 | | | | 49.84 | | 1 |
| nornir.funnel_append_batch | 1.23 | | | | 52137 | | 0 |
| nornir.knowledge_query | 16.85 | | 23742 | | | | 1 |
| nornir.knowledge_scan | 1.78 | | | 674582 | | | 0 |
| nornir.plan_dag_assemble | 107.38 | | | | | | 0 |
| nornir.symbol_append | 4.59 | 239618 | | | | | 0 |
| nornir.warehouse_query | 1.22 | 410561 | | | | | 0 |
<!-- nornir:gen:end:benches -->
<sub>These arms measure **nornir**, not knut. `knut/xtask` pins `nornir = "0.5"` from
crates.io, and that published runner predates the `NORNIR_BENCH_ONLY` arm filter — so the
filter was accepted and silently ignored, and nornir's suite ran alongside knut's. They
are kept in their own table so they can never be read as knut's numbers; the fix is a
newer runner, not an edit to this file.</sub>
## SQL for Power BI lives next door (2026-09-09)
knut's Flight SQL server (`graphar-flight`) speaks **Cypher** — the graph. Power BI, the
Arrow Flight SQL ODBC driver and ADBC speak **SQL**, and that dialect is
[**skade-flight**](https://git.gunnar.rs/nordisk/skade) in the skade
repo: DataFusion over the Iceberg warehouse, streamed Arrow, ODBC escapes translated, 129 Power Query
statement shapes green. Two dialects, two services, one wire; korp mounts both. The knut bridge —
graph tables as DataFusion table providers in that same session — is the next step, not done.
## Read the real docs
- 📖 **[The complete manual — `docs/manual.pdf`](docs/manual.pdf)** — every subsystem chapter (graphar, graphar-falkordb, graphar-flight, Bifröst, pipelines) plus the test & bench matrix.
- 📄 **[Full overview — `.nornir/README-full.md`](.nornir/README-full.md)** — the detailed pitch, architecture, quickstart, per-crate reference, and the full test matrix.
- 📝 [`docs/book.md`](docs/book.md) — the manual, as Markdown. Search it with `nornir docs search knut "<query>"`.