Research into whether the E2E harness could drop mando’s dev-gated CSV seed endpoint and insert test data through mando’s normal POST /data/insert from the client side. Answer: it is technically feasible — and rejected. This note records the wire format (which is unobvious and undocumented anywhere else), the proof that client-side construction works, and the drift argument that decided it.
Decision — keep the dev-gated CSV seed endpoint, reject client-side Arrow
Building the Arrow IPC body in mando-cli is possible (~250 LOC, ~17-20 new crates) and would remove the need for a mando-side test endpoint. It was rejected on drift risk: the body’s encoding is an undocumented internal of polars’
Deserializeimpl that has already changed between polars versions, and no test in either repo would guard the client-producing side. The CSV endpoint’s contract is stable, server-side, and covered by 14 tests that get fixed alongside any polars bump.
The wire format
POST /data/insert — handler mando-lib/src/app/route/save_data_route.rs.
{
"data_points": { "<data point id>": [80, 65, 82, 49, ...] },
"update_info": null
}The value per data point is an Arrow IPC byte stream serialized as a JSON array of decimal u8 values. Not base64. Not a binary body. A JSON array of numbers.
| Aspect | Fact |
|---|---|
| Payload type | Arrow IPC STREAM format (not the file format — no ARROW1 magic/footer) |
| JSON encoding | array of decimal u8, e.g. [80,65,82,49,...] |
| Server-side reader | polars-arrow (the arrow2 fork) StreamReader |
update_info | optional |
| Success | 204 No Content |
| Content-Type | never enforced — the handler takes body: String |
Why a JSON number array and nothing else
The shape is not a design choice, it is a consequence of polars 0.49.1’s Deserialize for DataFrame. Its deserialize_map_bytes visitor accepts bytes through visit_seq only when driven by serde_json — a base64 string fails. So the only JSON form the server can parse is the element-by-element number array.
Fixture proof
mando-lib/test/adapter/mando/rest/simple-send-test-data.json— 936 IPC bytes expand to ~9 KB of JSON (the file is 8941 bytes on disk). That expansion ratio is the fingerprint of this format; if you see it, you are looking at an IPC-in-JSON body.
Required DataFrame schema
Taken from the sibling CSV route mando-lib/src/app/route/save_data_csv_route.rs, which builds the same DataFrame the IPC path produces:
| Column | Type | Notes |
|---|---|---|
value_time | Timestamp(Microsecond, None) | naive, UTC-meaning |
generation_time | Timestamp(Microsecond, None) | naive, UTC-meaning |
fetch_time | Timestamp(Microsecond, None) | naive, UTC-meaning |
value (or value_x / value_y) | Float64 | |
id | Utf8 | matrix and trade data points only |
Rejected outright: a flag column, and any null cell.
Downstream, get_column_date_times ignores the timezone and normalises ns/us/ms, so microsecond-naive is the safe target rather than a hard requirement of the consumer.
Client-side construction is feasible — proven, not assumed
mando-repository/src/arrow.rs:59-88 (arrow_record_batch_to_polars_record_batch) already does exactly the round-trip in question: it writes with arrow-rs arrow_ipc::writer::StreamWriter and reads the result back with polars_arrow::io::ipc::read::StreamReader. Both sides are pinned together in mando’s Cargo.lock (arrow 56.2.0, polars =0.49.1), so arrow-rs stream output is known-good input for the exact reader /data/insert uses.
Costed options
| Option | Cost | Verdict |
|---|---|---|
arrow-ipc + arrow-array + arrow-schema in mando-cli | ~250 LOC, ~17-20 crates added to the distributed CLI | feasible, rejected (see below) |
Depend on polars directly | 100+ crates | rejected — absurd for a CLI that ships as a small musl binary |
| Ship pre-serialized IPC fixtures | none | rejected — the CSVs are the reviewable spec; opaque binary fixtures are not reviewable and not authorable by the team |
| Hand-roll the flatbuffer | high | rejected — untestable, silently goes stale |
The deciding factor: drift risk
This is the whole argument, and it is a repeat of a failure this project already suffered.
- The insert body’s encoding is not a published contract. It is an emergent property of polars’ internal
Deserializeimpl forDataFrame. - It already changed between
=0.45.1and=0.49.1. - If mando-cli produced the bytes, no test anywhere would guard the producing side — mando’s tests exercise its own reader, mando-cli’s tests would exercise its own writer, and nothing would put the two in the same process.
- That is the same failure class as the
DataPointIdserde drift (see mando-cli-e2e-live-green-2026-08-05): unit tests stayed green on both sides for weeks while the live wire 400’d.
Against that, the CSV endpoint is:
- a server-side contract, in the same repo as the polars dependency;
- covered by 14 tests that a polars bump breaks loudly and locally;
- expressed in the format the suites are already authored in.
Generalizable rule
Do not re-implement a wire format whose only specification is another crate’s private serde impl. If you must, the format’s owner and its producer have to live in the same test process — otherwise a dependency bump silently splits them.
Open improvement (not done)
The test endpoint is currently gated at runtime by MANDO_TEST_ENDPOINTS, meaning the code ships inside production binaries. It could be gated at compile time instead:
- add a cargo feature
test-endpoints; #[cfg(feature = "test-endpoints")]on the module declaration and on the route registration;- the CI e2e image build adds
--features test-endpoints.
Roughly 3 lines. The payoff is that test-only routes are absent from production binaries entirely rather than merely unreachable.
If client-side Arrow insert is ever revisited
The non-negotiable precondition is a contract test living in mando’s repo that round-trips an arrow-rs
StreamWriterstream throughserde_json::from_value::<DataFrame>. Without that single test, a polars bump breaks the CLI’s seeding with no local signal anywhere.
Minor finding
mando-cli seeds with Content-Type: text/csv. The server never reads it (the handler signature is body: String). The header is decorative — harmless, but do not treat it as part of the contract.
Related
- mando-cli-e2e-harness-2026-08-04 — where the CSV seed endpoint came from; its “why a new endpoint was unavoidable” callout is the claim this research tested and confirmed with the mechanism: the impossibility is the polars serde encoding, not Arrow itself.
- mando-cli-e2e-live-green-2026-08-05 — the
DataPointIdserde drift that is the precedent for the drift argument above. - mando-cli-junit-per-assertion-2026-08-06 — the other e2e work landed today.
- mando-e2e-ci-green-2026-08-05 — the CI job that runs the suites this endpoint seeds.