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

nordisk/nornir

.nornir/warehouse-trait-detach-design.md

Warehouse-trait detach — design & phase log

Goal. Lift nornir's warehouse fully behind its Warehouse trait so alternate backends (a thin RemoteWarehouse gRPC client, DuckDB, …) plug in without touching the ~150 sync block_on call sites. The trait is the durable seam; IcebergWarehouse is the default (and, until this pass, only) impl.

This file is the running phase log for that detach. It is additive & phased — nornir is large and central, so no phase is allowed to regress the working warehouse (L2: the ~150 call sites keep passing).

The seam as it actually stands (audit)

The trait exists (src/warehouse/mod.rs), and open() / open_read_only() already return Box<dyn Warehouse>. But the audit found the seam is nominal, not yet load-bearing, for three concrete reasons:

  1. The warehouse's OWN per-table modules are Iceberg-intrinsic, not accidentally coupled. ~40 free functions in src/warehouse/*.rs (release_events, saga_events, coverage, funnel, yggdrasil, test_results, …) take wh: &IcebergWarehouse and reach for wh.catalog() / wh.block_on() / wh.table_ident() to build Iceberg RecordBatches against the redb catalog. These ARE the backend's body — they are not the coupling to break; they are what a different backend would re-implement.

  2. ~50 named methods live INHERENT on IcebergWarehouse, off the trait. The trait deliberately kept only the generic Arrow core + two named wrappers (append_bench_run/query_bench_runs). Everything else consumers call — append_advisories, query_sbom_components, ingest_symbol_scans, root(), block_on(), append_mcp_calls, query_vuln_findings, … — is inherent. So a consumer holding &IcebergWarehouse cannot be flipped to &dyn Warehouse until the method it calls is first lifted onto the trait.

  3. Consumers construct the concrete type directly. Almost every reader (the viz/* tabs, funnel/store, docs, security) calls IcebergWarehouse::open_read_only(root) itself rather than receiving an injected Box<dyn Warehouse>. The open() box is barely used as the injection point.

Audit tally (grep over src/, excluding the impl file itself):

Phase 3 landed this pass — RemoteWarehouse (the seam proof)

src/warehouse/remote.rs — a SECOND concrete Warehouse impl, a thin gRPC client that owns no catalog and no Parquet, yet satisfies the same trait the call sites drive. This is what proves the trait is a real, detachable seam. Compiled behind the tonic stack (#[cfg(any(feature = "server", "viz", "mcp"))], all default) — no new dependency (reuses the already-generated nornir.v1 warehouse_client).

Wired to the existing generic-browser gRPC (proto/nornir.proto, service Warehouse):

trait method transport status
table_names Warehouse.Tables RPC wired
scan_preview Warehouse.Scan RPC (stringified rows) wired
scan_limited Warehouse.Scan (limit) → all-Utf8 RecordBatch wired
scan_arrow Warehouse.Scan (server default) → batch wired
scan_filtered — typed Unsupported (no pushdown on the preview RPC; TODO: Arrow Flight / filtered-scan RPC)
append_arrow, ensure_table, ensure_columns, append_bench_run — typed Unsupported (server is single-writer; submit via Telemetry.*)
query_bench_runs — typed Unsupported (needs a typed row over the wire; the preview is stringified)

The unwired methods return a typed RemoteWarehouseError::Unsupported { method, todo } — never a silent no-op (L2). The Warehouse.Scan RPC returns stringified cells (the display preview), so the reconstructed batch is honestly all-Utf8 — types are lost on the wire; a typed remote read (Arrow Flight / per-table RPC) is the documented follow-up.

Phase 2 (additive) landed this pass — scan_preview on the trait

Added fn scan_preview(&self, table, limit) -> Result<TablePreview> to the trait (re-exporting TablePreview from mod). This is the read seam that makes the generic warehouse browser source-agnostic: the Iceberg backend reads Parquet + stringifies; RemoteWarehouse maps the Warehouse.Scan response 1:1. The Iceberg trait impl delegates to the existing inherent scan_preview (inherent methods win method resolution, so it is NOT a recursion) — zero behavior change.

Tests (L4, RED-when-broken)

All prior warehouse tests stay green (cargo test --lib warehouse:: — see commit gate).

Deferred (the large sweep — logged with file:line in workspace_nordisk/nornir_agents_problems.md)