Gunnar

nordisk/skade

.nornir/catalog-key-zerocopy.md

<!-- nornir:class: reasoning -->
# catalog `table_key` — zero-copy (one alloc, byte-identical)

`keys::table_key(catalog, ident)` builds the redb lookup key
`"{catalog}\x1f{ns_joined_by_dot}\x1f{table}"`. It runs on **every** catalog op and
**once per commit** — a hot path the warehouse-write bench (`skade.funnel_append`-style)
flagged.

## The change
| | OLD (`format!`) | NEW (allocation-lean) |
|---|---|---|
| ns path | `ns.clone().inner().join(".")` → **clone the `Vec<String>` + a join `String`** | iterate `&[String]` **by reference** (NamespaceIdent derefs to `[String]`) |
| assemble | `format!("{cat}{SEP}{ns}{SEP}{name}")` → **another `String`** | one `String::with_capacity(exact)` + `push_str`/`push` |
| allocations | **2** (join + format) + a Vec clone | **1** (pre-sized) |

`ns_path` also dropped its `.clone().inner()` (now `ns.join(".")`) — benefits every key builder.

## Extended to the whole builder family (follow-up wave)

The same 2-alloc→1 pattern now covers **every** namespace/prefix builder in
`keys.rs`, factored through two zero-alloc helpers — `ns_path_len` (exact
byte-capacity, no allocation) and `push_ns_path` (append parts by reference):

| Builder | OLD | NEW | On the hot path of |
|---|---|---|---|
| `namespace_key` | `ns_path` join + `format!` (2) | 1 pre-sized `String` | every namespace lookup/create/update |
| `namespace_prop_key` | 2 | 1 | property get/set |
| `namespace_prop_prefix` | 2 | 1 | property range-scan bound |
| `table_prefix` | 2 | 1 | `list_tables` scan bound |
| `catalog_prefix` | `format!` machinery | `with_capacity`+push | catalog-wide scan bound |

Byte-identity pinned by `ns_keys_are_byte_identical_to_legacy_format`
(single/multi-part + non-ASCII parts, length via bytes). Micro-bench
`bench_catalog_key` now times all three shapes; light run on this box (dwarf,
release, N=200k): **namespace_key ~337→32 ns/op, table_key ~313→34, table_prefix
~252→33** (~8–10×). These are structural (fewer allocs), so kept regardless of
micro-noise; heavy corpus sweep → the quiet bench box (Loki/Odin), never here.

## Proof
- **Byte-identical (mandatory — it's a lookup key):** `keys::tests::table_key_is_byte_identical_to_legacy_format` pins the new builder against the exact old `format!` across single/multi-part namespaces + non-ASCII leaves. Always-on.
- **Faster:** in-crate `#[ignore]` micro-bench `bench_catalog_key` (run `-- --ignored --nocapture`): **legacy 513 ns/op → zero-copy 226 ns/op (~2.3×)** on this box (light, N=200k). Heavy corpus runs → the quiet bench box (Loki/Odin), never here.

## Harness bencher (`skade_catalog_key_build`) — the `bench`-feature hook (DONE)
`table_key` & family are `pub(crate)` — the catalog's internal on-disk encoding,
deliberately off the public API. The nornir-Bencher harness (its own
`skade-katalog-bench` crate) still needs to time them so the warehouse
no-regression gate has a real throughput number. That reach is now provided by a
**`bench`-feature pub hook**: `skade_katalog::bench_keys::*` re-exports the
builders as thin `#[inline]` pass-throughs **only** under `--features bench`. A
normal build (and `cargo publish`, which never enables it) neither widens the API
nor pays a byte — it only re-gates visibility of already-compiled fns, pulling no
extra deps.

- Hook: `src/lib.rs` `#[cfg(feature = "bench")] pub mod bench_keys`.
- Feature: root `Cargo.toml` `bench = []`; the bench crate depends on
  `skade-katalog = { path = "..", features = ["bench"] }`.
- Bencher: `bench/examples/nornir-bench.rs` `struct CatalogKeyBuild`
  (`id = "skade_catalog_key_build"`), batched timing (a ~30 ns op — per-iter
  `Instant::now()` would swamp it, so it times the whole loop and divides,
  `black_box`-pinning the builder + a live `sink`). Emits `ops_sec` (keyed off
  `table_key`, one per commit) plus per-builder `*_ns_per_op`. `LIGHT`,
  pure-in-proc (no containers/S3); the heavy N sweep goes to Loki/Odin via
  `BENCH_CATALOG_KEY_ITERS`.
- The always-on `#[ignore]`d in-crate `bench_catalog_key` micro-test stays as the
  zero-dependency local sanity check; the harness bencher is the CI/gate number.