Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/knut

.nornir/README-full.md

knut — complete overview

The real README: the full pitch, quickstart, design, examples and the measured test/bench matrix. The repo-root README.md is the slim face (title · mashup · pointers); this chapter is the heavy detail, and it is also folded into the manual docs/manual.pdf by nornir docs book.

knut

A pure-Rust columnar ↔ graph fabric: read/write Apache GraphAr files, bulk-load them into FalkorDB, and serve Cypher over Arrow Flight SQL — plus Bifröst, which streams Iceberg tables into a live graph by CDC (Spark-Connect or Spark-free). One Arrow pipe from the lakehouse to a queryable graph.

Why knut — it 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 ✗ ◐
Dual-engine CDC — Spark 4.2's native CHANGES clause and Iceberg's create_changelog_view, chosen by probing the live server ✅ NA ✗ ◐
CDC fallback is never silent — fell_back + the engine's own reason in the log and in state_json ✅ NA ✗ ✗
Renderable change rows — (op, before, after, snapshot, ts) Arrow, streamed batch-by-batch to a UI pane ✅ NA ◐ ✗
Projection + predicate pushed into the CDC read (not just full-table reads) ✅ 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 ◐ ✗ ✅ ✅

Full numbers — generated by nornir bench run knut. Rows marked NA: the rival has no such category.

New here? → knut for dummies — a hands-on, copy-paste tour that starts the containers and runs every feature.

Design docs. This README is the face. The deep design lives in the nornir doc set under .nornir/: the map and one territory doc per subsystem — graphar · graphar-falkordb · graphar-flight · knut-bifrost · knut-pipelines. Locked decisions and open work are in .nornir/plan.md; rendered docs (PDF + HTML) in docs/.

What it does

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        GraphAr files (disk)                     │
│   vertex/Person/id/chunk0.parquet                               │
│   edge/Person_knows_Person/ordered_by_source/adj_list/...       │
└───────────────┬─────────────────────────────────────────────────┘
                │ graphar (Rust)
                │ VertexReader / EdgeReader / VerticesBuilder / EdgesBuilder
                ▼
┌─────────────────────────────────────────┐
│   Apache Arrow RecordBatch (in-memory)  │
└───────────────┬─────────────────────────┘
                │ graphar-falkordb (Rust)
                │ FalkorDbLoader — UNWIND … CREATE (Cypher)
                ▼
┌─────────────────────────────────────────┐
│   FalkorDB  (Redis / Valkey module)     │
│   nodes with _gar_id property           │
└───────────────┬─────────────────────────┘
                │ graphar-flight (Rust)
                │ Arrow Flight SQL gRPC — GRAPH.QUERY → RecordBatch stream
                ▼
┌─────────────────────────────────────────┐
│   Arrow Flight SQL wire protocol        │
└───────────────┬─────────────────────────┘
                │ knut (Python)
                │ KnutClient — DoGet / DoPut
                ▼
┌─────────────────────────────────────────┐
│   pandas DataFrame  →  NetworkX Graph   │
│   nx.from_pandas_edgelist               │
│   nx.set_node_attributes                │
└─────────────────────────────────────────┘

Components

Rust crates

Crate Description
graphar GraphAr gar/v1 format — read & write chunked property graphs (5 formats, local or object store)
graphar-falkordb Bulk-load Arrow RecordBatches into FalkorDB
graphar-flight Arrow Flight SQL server — Cypher in, Arrow out; auth (bearer / Basic-handshake / mTLS); graph → lakehouse export (GraphAr file or Iceberg table)
knut-bifrost Bifröst 🌈 — watch Iceberg (Spark Connect or Spark-free via skade) or file drops, transform (incl. changelog CDC), sync to FalkorDB; Airflow AIP-82; file-or-Valkey state
knut-pipelines Viewer for Spark Declarative Pipelines + Spark Connect jobs — live DAG (egui-snarl) + job progress; knutpipe CLI/GUI
xtask nornir bench suite (cargo bench -p xtask)

Python client

Package Description
knut Flight SQL client — Cypher queries → pandas → NetworkX

Prerequisites

Dependency Version Notes
Rust stable ≥ 1.85 Rust 2024 edition
Python ≥ 3.11 for the knut client
FalkorDB latest Redis/Valkey graph module

Start FalkorDB with Docker:

podman run -p 6379:6379 localhost/knut-falkordb-valkey9:latest

Quick start

1. Write a graph to disk (GraphAr format)

use graphar::{
    VertexInfo, PropertyGroup, Property, DataType, FileType,
    VerticesBuilder, PropertyValue, Vertex,
};

let vi = VertexInfo::new("Person", 262_144)
    .with_property_group(PropertyGroup::new(
        vec![
            Property::new("id", DataType::Int64).primary(),
            Property::new("name", DataType::String),
        ],
        FileType::Parquet,
    ));

let mut builder = VerticesBuilder::new(vi, "./graph");
builder.add_vertex(Vertex::new()
    .add_property("id",   PropertyValue::Int64(0))
    .add_property("name", PropertyValue::String("Alice".into())));
builder.dump()?;

2. Load into FalkorDB

use graphar_falkordb::FalkorDbLoader;

let mut loader = FalkorDbLoader::connect("redis://127.0.0.1:6379", "mygraph").await?;
loader.create_index("Person", "_gar_id").await?;
loader.insert_vertices("Person", &batches).await?;
loader.insert_edges("Person", "KNOWS", "Person", &edge_batches).await?;

3. Start the Flight SQL server

use std::sync::Arc;
use graphar_flight::{SchemaRegistry, run_server};

// Empty registry works — types are inferred automatically from FalkorDB's response.
// Optionally register queries for explicit typed schemas.
let registry = Arc::new(SchemaRegistry::new());

registry.register_vertex_query(
    "MATCH (n:Person) RETURN n._gar_id AS _gar_id, n.name",
    &["name"],
);
registry.register_edge_query(
    "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a._gar_id AS src, b._gar_id AS dst",
    &[],
);

run_server("redis://127.0.0.1:6379", "mygraph", registry, "0.0.0.0:50051".parse()?).await?;

4. Query from Python → NetworkX

from knut import KnutClient

G = KnutClient().to_networkx(
    vertex_query="MATCH (n:Person) RETURN n._gar_id AS _gar_id, n.name",
    edge_query="MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a._gar_id AS src, b._gar_id AS dst",
)
print(G.nodes[0])  # {'name': 'Alice'}

The _gar_id linking key

All components share a single identifier: _gar_id, the 0-based row index of each vertex across all batches. It is the glue between GraphAr's internal vertex ID, FalkorDB node properties, and NetworkX node IDs.

GraphAr row index 0  ──►  FalkorDB node { _gar_id: 0, name: "Alice" }
                                │
            edge { src: 0, dst: 1 }  ──►  MATCH (a { _gar_id: 0 }), (b { _gar_id: 1 })
                                │
                     NetworkX node 0  with attrs { name: "Alice" }

graphar

Implements the GraphAr gar/v1 specification for reading and writing property graphs as chunked columnar files.

Concepts

File layout

<prefix>/
  <graph>.graph.yml
  <label>.vertex.yml
  vertex/<label>/<prop_group>/chunk0.parquet
  <src>_<edge>_<dst>.edge.yml
  edge/<src>_<edge>_<dst>/ordered_by_source/adj_list/part0/chunk0.parquet
  edge/<src>_<edge>_<dst>/ordered_by_source/offset/chunk0.parquet
  edge/<src>_<edge>_<dst>/ordered_by_source/<prop_group>/part0/chunk0.parquet

part{n} is the vertex-chunk partition index; chunk{n} is the edge-chunk index within that partition.

Writing a graph

use graphar::{
    AdjListInfo, AdjListType, DataType, Edge, EdgeInfo,
    EdgesBuilder, FileType, Property, PropertyGroup, PropertyValue,
    Vertex, VertexInfo, VerticesBuilder,
};

// Define vertex schema
let vertex_info = VertexInfo::new("Person", 100)
    .with_property_group(PropertyGroup::new(
        vec![
            Property::new("id", DataType::Int64).primary(),
            Property::new("name", DataType::String),
        ],
        FileType::Parquet,
    ));

// Write vertices
let mut vb = VerticesBuilder::new(vertex_info.clone(), "./data");
vb.add_vertex(Vertex::new()
    .add_property("id",   PropertyValue::Int64(0))
    .add_property("name", PropertyValue::String("Alice".into())));
vb.dump()?;

// Define edge schema (CSR — ordered by source vertex)
let edge_info = EdgeInfo::new("Person", "knows", "Person", 1024, 100, 100, false)
    .with_adj_list(AdjListInfo {
        ordered: true,
        aligned_by: "src".into(),
        file_type: FileType::Parquet,
        prefix: None,
    });

// Write edges
let mut eb = EdgesBuilder::new(edge_info, "./data", AdjListType::OrderedBySource, 2);
eb.add_edge(Edge::new(0, 1));
eb.dump()?;

Reading a graph

use graphar::{GraphInfo, VertexReader};

let graph_info = GraphInfo::load("./data/my_graph.graph.yml")?;
let vertex_info = graph_info.get_vertex_info("Person", "./data")?;

let reader = VertexReader::new(vertex_info.clone(), "./data", 1000);
let group = &vertex_info.property_groups[0];
let batches = reader.read_property_group(group)?;   // Vec<RecordBatch>

for batch in &batches {
    println!("{} rows", batch.num_rows());
}

Supported file formats

Format Read Write
Parquet ✓ ✓
CSV ✓ ✓
ORC ✓ ✓
JSON (lines) ✓ ✓
Arrow IPC ✓ ✓

All five formats work both on the local filesystem (graphar::io) and against any object store (graphar::storage::ChunkStore): S3, GCS, Azure, plus S3-compatible endpoints — SeaweedFS, RustFS, MinIO — via ChunkStore::s3_compatible(endpoint, bucket, key, secret, prefix).

Supported data types

Bool, Int32, Int64, Float, Double, String, Date, Timestamp, Time, List


graphar-falkordb

Bulk-loads Apache Arrow RecordBatches into a FalkorDB graph (Redis/Valkey) using batched UNWIND … CREATE Cypher queries.

How it works

Each GRAPH.QUERY call sends up to 512 rows (configurable) as an inline UNWIND list:

UNWIND [{`_gar_id`: 0, `name`: 'Alice'}, {`_gar_id`: 1, `name`: 'Bob'}] AS row
CREATE (n:`Person`) SET n = row

For edges, the server MATCHes the endpoints by _gar_id and sets edge properties:

UNWIND [{`_src`: 0, `_dst`: 1, `since`: '2020'}] AS row
MATCH (a:`Person` {`_gar_id`: row._src}), (b:`Person` {`_gar_id`: row._dst})
CREATE (a)-[r:`KNOWS`]->(b) SET r = row
REMOVE r._src, r._dst

Usage

use graphar_falkordb::FalkorDbLoader;

let mut loader = FalkorDbLoader::connect("redis://127.0.0.1:6379", "mygraph").await?;

// Index _gar_id before inserting edges — makes MATCH fast
loader.create_index("Person", "_gar_id").await?;

// Insert vertices — each RecordBatch column becomes a node property
loader.insert_vertices("Person", &vertex_batches).await?;

// Insert edges — batch must have Int64 `src` and `dst` columns
loader.insert_edges("Person", "KNOWS", "Person", &edge_batches).await?;

Merging GraphAr property groups before insert

GraphAr stores properties split across multiple group files. Merge them horizontally before inserting into FalkorDB:

VertexReader  →  [PropertyGroup RecordBatch, ...]
                        ↓
           merge_property_groups(groups)        — or reader.read_merged()
                        ↓
     FalkorDbLoader::insert_vertices(label, &[merged_batch])

Batch size

Default is 512 rows per GRAPH.QUERY call. Tune with .with_batch_size(n)?.

Arrow → Cypher type mapping

Arrow type Cypher literal
Int32 / Int64 integer, e.g. 42
Float32 / Float64 float, e.g. 3.14
Boolean true / false
Utf8 quoted string, e.g. 'Alice'
null null

graphar-flight

An Apache Arrow Flight SQL gRPC server that accepts Cypher queries instead of SQL, executes them against FalkorDB via GRAPH.QUERY, and streams results as Arrow RecordBatches.

Any Flight SQL-compatible client works: ADBC, DataFusion, DBeaver (Flight plugin), flightsql-dbapi (Python), the knut Python client included in this repo, etc.

Protocol flow

Client                              Server (graphar-flight)
  │                                        │
  │── GetFlightInfo(CommandStatementQuery) ─►│ look up schema in registry
  │◄─ FlightInfo(schema, ticket) ───────────│
  │                                        │
  │── DoGet(ticket) ───────────────────────►│ GRAPH.QUERY → RecordBatch
  │◄─ FlightData stream ────────────────────│
  │                                        │
  │── DoPut(CommandStatementUpdate,        │
  │         Arrow stream) ────────────────►│ UNWIND … CREATE in FalkorDB
  │◄─ affected row count ───────────────────│

Schema registry (optional)

The server has two modes:

Mode How When to use
Auto (default) Column names and types inferred from the FalkorDB response. Int → Int64, Double → Float64, Boolean → Boolean, strings → Utf8. Works for any query with zero setup.
Registered Schema pre-declared via register_vertex_query, register_edge_query, or register. When you want guaranteed types regardless of data, or need to catch schema drift early.

In auto mode the server peeks at the first non-null value in each column to determine its Arrow type, so integer IDs like _gar_id, src, and dst arrive as Int64 by default — correct NetworkX node IDs with no configuration needed.

Registering typed schemas (optional, but faster)

Two convenience helpers cover the common patterns. Both set _gar_id, src, and dst to Int64 (native integers, not strings), and append any extra column names as Utf8.

use std::sync::Arc;
use graphar_flight::{SchemaRegistry, run_server};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // An empty registry is valid — unregistered queries use auto-string mode.
    let registry = Arc::new(SchemaRegistry::new());

    // Vertex query: _gar_id → Int64, extra columns → Utf8.
    // The RETURN clause must alias the internal ID as _gar_id and list
    // extra columns in the same order as the slice.
    registry.register_vertex_query(
        "MATCH (n:Person) RETURN n._gar_id AS _gar_id, n.name, n.age",
        &["name", "age"],
    );

    // Edge query: src + dst → Int64, extra columns → Utf8.
    // Alias source _gar_id as src, destination _gar_id as dst.
    registry.register_edge_query(
        "MATCH (a:Person)-[r:KNOWS]->(b:Person) \
         RETURN a._gar_id AS src, b._gar_id AS dst, r.since",
        &["since"],
    );

    // Edges with no properties — just topology.
    registry.register_edge_query(
        "MATCH (a:Person)-[:FOLLOWS]->(b:Person) \
         RETURN a._gar_id AS src, b._gar_id AS dst",
        &[],
    );

    // Full control: use register() directly for custom types (e.g. age as Int64).
    // registry.register("...", Arc::new(Schema::new(vec![...])));

    run_server(
        "redis://127.0.0.1:6379",
        "mygraph",
        registry,
        "0.0.0.0:50051".parse()?,
    ).await?;

    Ok(())
}

Bulk insert (DoPut)

The CommandStatementUpdate query string is a directive, not a Cypher statement:

Directive Action
VERTICES:<label> Insert vertices from the Arrow stream
EDGES:<src>:<type>:<dst> Insert edges (stream must have src + dst Int64 columns)

Redis → Arrow type mapping

Both registered and auto-string modes use the same coercions. In auto-string mode the type is inferred from the first non-null value in each column.

Redis RESP value Arrow column type
Int Int64
Double Float64
Boolean Boolean
BulkString / SimpleString Utf8
Nil null (type inferred from next non-null row; falls back to Utf8 if all null)

Integer properties like _gar_id, src, and dst arrive as Int64 in both modes — no registration needed for correct NetworkX node IDs.


Python client (knut)

Located in python/. A pure-Python Arrow Flight SQL client that queries the graphar-flight server and builds NetworkX graphs directly from the Arrow results.

No intermediate format conversion is needed: Arrow → pandas → NetworkX uses nx.from_pandas_edgelist and nx.set_node_attributes directly.

Install

cd python
pip install -e ".[dev]"   # installs pyarrow, pandas, networkx

Query → NetworkX

No schema pre-registration is required. The server infers Arrow types from FalkorDB's response — integers arrive as Int64, floats as Float64, strings as Utf8. NetworkX node IDs are integers by default.

from knut import KnutClient

client = KnutClient(host="localhost", port=50051)

# One call: fetch vertices + edges, return a NetworkX DiGraph
G = client.to_networkx(
    vertex_query="MATCH (n:Person) RETURN n._gar_id AS _gar_id, n.name, n.age",
    edge_query=(
        "MATCH (a:Person)-[r:KNOWS]->(b:Person) "
        "RETURN a._gar_id AS src, b._gar_id AS dst, r.since"
    ),
)

print(G.number_of_nodes(), G.number_of_edges())
print(G.nodes[0])   # {'name': 'Alice', 'age': 30}
print(G[0][1])      # {'since': '2020-01-01'}

# Standard NetworkX algorithms work on the result
print(nx.in_degree_centrality(G))
print(nx.shortest_path(G, source=0, target=3))

Insert

import pandas as pd
from knut import KnutClient

client = KnutClient()

# Vertices — _gar_id is added automatically (0-based row index)
client.insert_vertices("Person", pd.DataFrame({
    "name": ["Alice", "Bob", "Carol"],
    "age":  [30, 25, 35],
}))

# Edges — src/dst reference _gar_id values of the endpoint nodes
client.insert_edges("Person", "KNOWS", "Person", pd.DataFrame({
    "src":   [0, 0, 1],
    "dst":   [1, 2, 2],
    "since": ["2020-01-01", "2019-06-15", "2021-03-10"],
}))

Raw Arrow query

import pyarrow as pa

table: pa.Table = client.query("MATCH (n:Person) RETURN n._gar_id AS _gar_id, n.name")
df = table.to_pandas()

Proto encoding

The client speaks raw gRPC Flight SQL without any generated proto files or grpc_tools.
knut/_proto.py hand-encodes the two Flight SQL messages used
(CommandStatementQuery, CommandStatementUpdate) with a small varint
wire-format encoder — the only dependency is pyarrow.

API reference

Method Description
KnutClient(host, port, tls) Connect to a graphar-flight server
query(cypher) Run a registered Cypher query → pyarrow.Table
query_df(cypher) Run a registered Cypher query → pandas.DataFrame
insert_vertices(label, df) Bulk-insert vertices via DoPut
insert_edges(src, type, dst, df) Bulk-insert edges via DoPut
to_networkx(vertex_query, edge_query, ...) Fetch graph → networkx.Graph

Tests

cd python
pytest tests/ -v   # 13 tests, no server required (Rust: 19 unit + 6 doctests)

Building & testing

# Rust — build and test all crates
cargo build
cargo test

# Python — install and test
cd python
pip install -e ".[dev]"
pytest tests/ -v

Running the full stack locally

# 1. Start FalkorDB
podman run -p 6379:6379 localhost/knut-falkordb-valkey9:latest

# 2. Build and run the Flight SQL server (see graphar-flight example above)
cargo run --example server

# 3. Run the Python demo
cd python
python examples/social_graph.py

Bifröst 🌈 — Iceberg tables → FalkorDB graph

knut-bifrost watches Iceberg 1.11+ tables and continuously materialises them as a property graph: snapshot polling, incremental reads, a declarative-or-custom transform fabric, and an idempotent MERGE sink.

Two read lanes: Spark-free (IcebergSource, feature skade — pure Rust, no JVM, a local warehouse directory) and Spark Connect (SparkConnectSource, feature spark — the only route to a remote REST/Glue/Hive catalog or an S3-backed warehouse). Both are library-only; the bifrost CLI wires FileSource only. Which one to pick, what must be running for each, and the transfer-method menu into FalkorDB: iceberg-falkordb-guide.md. Full design: bifrost-design.md.

# bifrost.yml
version: 1
tables:
  - table: lake.social.users
    mode: incremental
    pipeline:
      - vertex: { label: Person, id_column: user_id, properties: [name] }
      - fk_edges:
          type: WORKS_AT
          src: { label: Person,  column: user_id }
          dst: { label: Company, column: company_id }
let source = SparkConnectSource::connect("sc://spark:15002").await?; // feature "spark"
let sink   = FalkorDbSink::connect("redis://127.0.0.1:6379", "social").await?;
let mut l  = Listener::new(MappingSpec::load("bifrost.yml")?, TransformRegistry::new(),
                           source, sink, SyncState::open("state.json")?);
l.run(Duration::from_secs(30), |round| { println!("{round:?}"); true }).await?;

Test matrix

Unit tests (cargo test --workspace, no containers) and a podman-backed integration matrix (-- --ignored, via testcontainers over the podman socket). Container images for the matrix live in containers/.

Layer Unit tests Live containers (--ignored)
graphar formats round-trip × {parquet, csv, json, orc, arrow-ipc} + empty-input —
graphar metadata YAML round-trips, chunk-path formatting (vertex/edge/offset/property) —
graphar merge two-group row-aligned merge, missing-column errors —
graphar storage ChunkStore local round-trip × 5 formats, raw bytes SeaweedFS + RustFS S3 round-trips × 5 formats
graphar-falkordb Cypher rendering, UNWIND query shape, batch splitting live load + Cypher count/property checks vs FalkorDB and FalkorDB-on-Valkey 9 (KNUT_FALKORDB_IMAGE)
graphar-flight auto-schema inference, conversion, registry, named-table resolution, SELECT … FROM rewrite, auth (bearer/Basic/open), skade Iceberg-export round-trip (no container) Power BI path: live FalkorDB + Basic handshake → bearer, GetCatalogs/GetDbSchemas/GetTables/GetSqlInfo, SELECT * FROM <named query> via the stock FlightSqlServiceClient; graph → GraphAr export round-trip
knut-bifrost transforms, mapping YAML→pipeline, custom registry, state ledger, FileSource scan/ledger, Airflow asset-event model, CDC change-row partitioning + DETACH DELETE, skade IcebergSource read + snapshot tracking (no container), create→detect→ingest lifecycle matrix + a subprocess test that drives the bifrost CLI and asserts its JSON log events TOP GUN: Spark 4.1.2 Connect + Iceberg 1.11.0 → bifröst → FalkorDB (initial + idle + incremental); OSM relations.parquet → FileSource → FalkorDB; real-FalkorDB lifecycle; Valkey shared-ledger across instances
knut-pipelines DAG model (validate/topo), progress + ViewerState folding, proto conversion, snarl build live Spark 4.2: execute_sql job progress + a full define_graph → StartRun SDP run (QUEUED→…→COMPLETED)
# build the matrix images once
podman build -t knut-falkordb-valkey9 -f containers/falkordb-valkey9/Containerfile .
podman build -t knut-spark-iceberg    -f containers/spark-iceberg/Containerfile .

# run everything that needs podman
cargo test --workspace -- --ignored
cargo test -p knut-bifrost --features spark --test integration_spark_falkordb -- --ignored

Benchmarks

knut follows the nornir bench contract — one suite, single-shot real workloads, one BenchRun JSON line for the warehouse. See .nornir/benching.md.

cargo run -p xtask --release --example nornir-bench   # canonical
cargo bench -p xtask                                  # same suite

Roadmap

Item Status
GraphAr Parquet + CSV read/write ✓ done
FalkorDB bulk insert via Cypher UNWIND ✓ done
Arrow Flight SQL server (Cypher dialect) ✓ done
Python client → NetworkX ✓ done
merge_property_groups / VertexReader::read_merged ✓ done
ORC + JSON + Arrow IPC file formats ✓ done
Object storage (S3 / GCS / Azure / SeaweedFS / RustFS via object_store) ✓ done
Flight SQL authentication (TLS + bearer token) ✓ done
Bifröst: Spark Connect + Iceberg → transform fabric → FalkorDB ✓ done
Container test matrix (FalkorDB-on-Valkey 9, Spark 4.1.2 + Iceberg 1.11, SeaweedFS, RustFS) ✓ done
nornir bench suite ✓ done
Airflow 3 asset events (AIP-82): emit + FileSource listener ✓ done
Power BI / ADBC via Flight SQL metadata surface (catalogs, schemas, tables, SqlInfo, prepared statements) ✓ done
Flight SQL auth: bearer + Basic/handshake (Power BI/ODBC) + mutual TLS ✓ done
Bifröst changelog CDC (Iceberg _change_type → upserts + DETACH DELETE) ✓ done
Spark-free Iceberg source via skade (IcebergSource, snapshot-incremental) ✓ done
Graph → lakehouse export: GraphAr file + Iceberg table (via skade) ✓ done
Valkey-backed shared SyncState + graceful shutdown ✓ done
Date/Timestamp/List → Cypher rendering ✓ done
Spark Declarative Pipelines viewer (knutpipe pipeline: define + run + stream) ✓ done
WASM / Flight transform plugins; REST Iceberg catalog; Iceberg-table CDC export proposed

The full proposition list with rationale: .nornir/plan.md.


License

Apache-2.0