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

nordisk/knut

.nornir/plan.md

Plan — proposed functionality

What knut should grow next, roughly ordered by leverage. Items marked ⭐ are the ones that change what knut is, not just what it has.

⭐ 1. Airflow 3 asset events (AIP-82) — the file/table listener as a citizen of the DAG world ✓ DONE (2026-06-10)

Shipped: knut_bifrost::airflow (AssetEvent, AssetEmitter, round_to_events, emit_round, and the feature-gated AirflowEmitter POSTing to /api/v2/assets/events) + knut_bifrost::source::FileSource, a TableSource over a watched directory whose TableState::done_files ledger is the "which files are done" bookkeeping. Unit-tested; OSM e2e in tests/integration_osm_filesource.rs. Detail below = future depth.

Airflow 3's event-driven scheduling (AIP-82, AssetWatcher + BaseEventTrigger) lets a DAG run when an external event lands, not on a cron. Bifröst already has the two halves AIP-82 wants:

Concretely: a small knut-airflow crate (or a bifrost --emit-airflow flag) plus a documented AssetWatcher recipe on the Airflow side.

⭐ 2. Power BI over Flight SQL (ADBC) ✓ DONE (2026-06-10)

Shipped in graphar-flight: the metadata surface (GetCatalogs, GetDbSchemas, GetTables, GetTableTypes, GetSqlInfo, prepared statements) with registered queries surfaced as tables (named via SchemaRegistry::register_named) and SELECT * FROM <table> rewritten to the underlying Cypher. Auth covers what Windows BI clients use: bearer, Basic + Flight handshake (token issuance), and mutual TLS — see graphar_flight::auth::AuthConfig and ServerOptions. Unit-tested; live path in tests/integration_flight_sql.rs via the stock FlightSqlServiceClient.

Power BI switched new connections to ADBC by default in February 2026 (full transition planned July 2026), and Dremio already ships an ADBC/Flight-SQL-based Power BI connector — the wire knut already speaks. To get knut dashboards in Power BI:

3. Iceberg source (drop Spark for append-only tables) ✓ DONE (2026-06-11)

Shipped: IcebergSource (feature skade) — a TableSource that reads an Iceberg table directly through skade's embedded catalog, no Spark. Each poll compares the table's current snapshot id (inner().metadata().current_snapshot()) to the recorded one and re-reads on change (idempotent MERGE makes that safe). skade and knut now share one Arrow 58 tree, so the seam (bridge_57_to_56, mirroring graphar-flight's bridge_56_to_57) is a plain identity clone(); it began as a zero-copy Arrow C Data Interface transmute when skade was on 57 and knut on 56 (see reasoning.md). From crates.io (skade = { version = "0.5", default-features = false }), no path coupling — and, since skade 0.5.0 published the equality-delete surface, no root [patch.crates-io] sibling override either. Unit-tested (no container) in source::iceberg::tests. Embedded catalog, not REST — but Spark-free, which was the point; a REST catalog could swap in behind the same Warehouse/iceberg-rust later.

iceberg-rust can read snapshots + parquet data files directly from the catalog; for append-only tables bifröst wouldn't need a Spark cluster at all. Same TableSource trait, much smaller footprint.

4. Changelog-view incremental reads (updates + deletes) ✓ DONE (2026-06-11)

Shipped: a new SyncMode::Cdc. SparkConnectSource reads Iceberg's system.create_changelog_view (carrying _change_type) instead of EXCEPT ALL; transform::partition_change_rows splits each round into upserts (INSERT/UPDATE_AFTER) and deletes (DELETE/UPDATE_BEFORE), strips the sentinel column, and GraphMutation.deletes (a VertexDelete set) drives MATCH (n:Label {id}) DETACH DELETE n in the sink (run after upserts so a same-round upsert+delete resolves to gone). SinkReport.deleted + a vertices_deleted log event. Caveat: edge-only changelog deletes are out of scope (a vertex DETACH DELETE already removes its incident edges).

EXCEPT ALL only sees appends. Iceberg's system.create_changelog_view exposes INSERT/UPDATE/DELETE row-level diffs; map DELETE → MATCH … DETACH DELETE in the sink and bifröst becomes a real CDC consumer.

5. Graph → lakehouse round trip ✓ DONE (2026-06-11)

Shipped (part 1): graphar_flight::export::export_cypher writes a Cypher query's rows as a GraphAr chunk file (FalkorDB → Arrow → lakehouse). Runnable via cargo run -p graphar-flight --example export; round-trip in tests/integration_export.rs.

Shipped (part 2, feature skade): export_cypher_to_iceberg appends the rows straight into an Iceberg table via skade (the Iceberg read/write stack over an embedded catalog). skade and knut now share one Arrow 58 tree, so bridge_56_to_57 is a plain identity clone() (an Arc bump — no temp file, no serialization); it was a zero-copy Arrow C Data Interface transmute back when skade was on 57 and knut on 56 (see reasoning.md). append_arrow_to_iceberg_via_parquet keeps the old temp-parquet path as the baseline the bridge beats (≈2× — see the two knut.skade_iceberg_export* benchers in benching-benchmarks.md). Runnable via --features skade --example export-iceberg; the bridge + write/read-back is unit-tested (no container) in export::skade_tests.

Shipped (part 3): export_cypher_chunked(exec, cypher, dir, FileType, chunk_rows) writes the result as multiple GraphAr chunk files (chunk0.<ext>, chunk1.<ext>, …) of ≤ chunk_rows rows each, so write-side/encoder memory and per-file size stay bounded by chunk_rows for huge results; ExportReport gained an additive chunks: Vec<PathBuf>. Slice-and-write, not pull-stream: FalkorDB's GRAPH.QUERY is one RESP reply with no server-side cursor, so the full result is fetched once and then zero-copy-sliced (RecordBatch::slice shares buffers) — true streaming from FalkorDB would need a cursor API RESP doesn't expose. Round-tripped (no container) in export::chunk_tests: N files for chunk_rows < total, each bounded, total rows preserved.

The reverse arrow (now done): export_cypher writes a query's rows back as a GraphAr file and export_cypher_to_iceberg straight into an Iceberg table, so knut covers both directions of the columnar ↔ graph bridge.

6. WASM / Flight transform plugins ✓ DONE (2026-06-11)

Shipped: knut_bifrost::wasm::WasmTransform (feature wasm) — a Transform whose logic is a sandboxed wasm module run by the pure-Rust wasmi interpreter (no C/JIT). ABI: the guest exports memory + alloc(len)->ptr + transform(ptr,len)->packed(ptr,len); the host serializes input batches to Arrow IPC, hands them in, reads the IPC result back, and turns each output batch into a VertexSet — same GraphMutation contract, now polyglot + out-of-line. from_bytes/from_file; tested with an inline WAT identity guest (rows round-trip through the IPC bridge).

Real wasm32 guest example shipped (2026-06-11): crates/knut-bifrost/wasm-guest/. A standalone cdylib crate (NOT a workspace member — listed in [workspace] exclude, carries its own empty [workspace] table) that compiles to wasm32-unknown-unknown and implements a real arrow-on-wasm transform: read the input Arrow IPC stream (arrow_ipc::reader::StreamReader), keep only rows where _gar_id is even (arrow_select::filter::filter_record_batch), write the result back as an IPC stream (StreamWriter) into a leaked buffer, and return the packed (out_ptr<<32)|out_len. Exports memory + alloc (Vec-leak) + transform, all #[no_mangle] pub extern "C". Arrow 58 compiles to wasm32-unknown-unknown out of the box — no patches/shims/feature gymnastics (arrow-array/-schema/-buffer/-data/-ipc/-select + flatbuffers, chrono, half, ahash, getrandom 0.2, num-* all build clean for bare-unknown). Build: cargo build -p knut-bifrost-wasm-guest --target wasm32-unknown-unknown --release --manifest-path crates/knut-bifrost/wasm-guest/Cargo.toml → wasm-guest/target/wasm32-unknown-unknown/release/knut_bifrost_wasm_guest.wasm (~1.2 MB, opt-level="z"+lto+panic="abort"). An #[ignore] integration test (tests/wasm_guest.rs, feature wasm) loads the artifact into a WasmTransform and asserts only even-_gar_id rows survive — run with cargo test -p knut-bifrost --features wasm --test wasm_guest -- --ignored (ignored by default since it needs the pre-built .wasm). See wasm-guest/README.md.

Flight half shipped (2026-06-11): knut_bifrost::flight::FlightTransform (feature flight). A Transform that proxies the transform to an external Arrow-Flight service over DoExchange (the bidirectional Flight stream): connect(endpoint, label, id_column) opens a tonic channel; each apply encodes the input &[RecordBatch] as a Flight data stream (FlightDataEncoderBuilder), do_exchangees it, decodes the returned batches (FlightRecordBatchStream), and turns each into a VertexSet{label,id_column} — the same GraphMutation contract as WasmTransform, but out-of-process and polyglot (the service can be any language with an Arrow Flight server). Transform::apply is sync, so it bridges to the async FlightClient with a small ambient-runtime-aware block_on (hands the future to a dedicated current-thread runtime when already inside a Tokio runtime, so a current-thread worker can't deadlock). All transport errors map to BifrostError::Other prefixed flight:. Pulls arrow-flight + tonic (workspace versions, same 56/0.13 tree as graphar-flight) only under the off-by-default flight feature — the default build's dep tree is unchanged. Tested with no external infra: an in-process echo DoExchange FlightService is bound on an ephemeral port (TcpIncoming) on a background task, a FlightTransform points at it, and apply over a sample batch is asserted to round-trip the rows into GraphMutation.vertices (flight::tests, gated on feature = "flight"). #6 now fully done.

The fabric's escape hatch is compile-time Rust today. A WASM host (transform = module taking/returning Arrow IPC bytes) or an external Arrow Flight transform service would allow out-of-process, polyglot transforms with the same GraphMutation contract.

7. Operational hardening

8. Spec completeness


Handoff state (2026-06-09, end of session)

Handoff state (2026-06-10)

Handoff state (2026-06-11) — current

Everything is committed and pushed (codeberg.org:nordisk/knut, branch main). All §-marked roadmap items are done except where noted: