---
summary: "Foundational dialect-bypass fix — every BrAPI client GET routes through the dialect adapter, with tight companion-call budgets so slow upstreams no longer 4× the response time."
breaking: false
---

# 0.4.7 — 2026-05-01

The v0.4.6 release wired the dialect adapter into the `find_*` surface and one `get_germplasm` probe. Every other call site that built GET filter params — `ReferenceDataCache` (programs / trials / locations), `get_study`'s observation / observation-unit / variable count probes, and `submit_observations`'s preflight — bypassed the dialect entirely and shipped the v2.1 plurals to the wire. On SGN-family servers (CassavaBase, Breedbase) those plurals are silently ignored: `/trials?trialDbIds=7526` returns the unfiltered set's row zero (`5CP_Kenya / trialDbId 1911`), and `/observations?studyDbIds=X` triggers a full table scan that exceeds the 30 s timeout. With the global retry budget that meant 4 × 30 s ≈ 2 minutes of waiting per failing companion, every time.

This release pushes the dialect adapter to the `BrapiClient` edge so every GET — current and future — translates plurals to per-server singulars without each call site having to remember. Companion enrichments now run with their own tight budget (8 s timeout, 0 retries) so a slow upstream surfaces as a single warning instead of stretching the response.

## Added

- **`BrapiRequestOptions.dialect`** — when supplied, `BrapiClient.get` extracts the endpoint segment from the path and routes `params` through `dialect.adaptGetFilters` before serializing. Single source of truth for plural/singular translation across every call site. `raw_get` continues to opt out by omitting the field.
- **`BrapiRequestOptions.warnings`** — sink for dialect translation warnings (downcast, dropped). Threaded through to the same `warnings[]` array the calling tool surfaces in its envelope.
- **`BrapiRequestOptions.retryMaxAttempts`** — per-call override of the global retry budget. Companion calls set it to `0` so a slow upstream surfaces as one warning rather than 4× the per-attempt timeout.
- **`DIALECT_ALL_DROPPED_REASON`** + `isDialectAllDropped(err)` — stable error-code constant and type guard. Companion call sites catch and convert to "skip with warning" instead of propagating; find-style call sites continue to throw via their `all_filters_dropped` typed contract.
- **`companionRequestOptions(connection, dialect, config, warnings, params?)`** in `find-helpers` — bundles the companion-call shape (tight timeout + zero retries + dialect + warnings) so future companion sites stay consistent.
- **`BRAPI_COMPANION_TIMEOUT_MS`** env var (default 8 s) — per-call timeout for non-critical enrichments. Falls under the existing `BRAPI_REQUEST_TIMEOUT_MS` (30 s) which still governs the main path.
- **`observationunits` mapping** added to the `cassavabase` and `brapi-test` dialects — `studyDbIds → studyDbId` plus the rest of the cross-FK plurals so observation-unit count probes reach the wire as singulars.

## Changed

- **`ReferenceDataCache.fetchBatch`** threads `dialect` through to the client. On `dialect_all_filters_dropped` returns an empty Map and pushes a warning into the supplied sink — caller treats the FK as unavailable instead of receiving a page-zero row keyed off the wrong ID. Retry budget set to 0; timeout to `companionTimeoutMs`. **CassavaBase trial/program/location FK lookups now actually filter** instead of returning row zero.
- **`brapi_get_study`** companion probes (`/observations`, `/observationunits`, `/variables`) now use `companionRequestOptions(...)` and route through the dialect. The screenshot-class failure mode is fixed: where v0.4.6 burned ~2 minutes retrying `?studyDbIds=12642` on cassavabase (silently ignored, full table scan), v0.4.7 sends `?studyDbId=12642` (15 s on cassavabase, no retry). Total wall time on the same study drops from ≥120 s to <20 s on the worst path. The companion failure mode also reverts cleanly — when the upstream is genuinely slow the call surfaces a "probe failed" warning instead of pinning the response.
- **`brapi_submit_observations`** post-write count probe routes through the dialect with the same companion budget. `ApplyBranchSchema` gains an optional `warnings: string[]` field for connection-level diagnostics from the dialect translation.
- **`BrapiClient.get`** is now `async` so synchronous throws from the dialect adapter (the all-dropped throw) become rejected promises rather than escaping the call stack.

## Fixed

- **CassavaBase trial / program / location FK lookups** no longer return wrong rows. Before: `GET /trials?trialDbIds=7526` upstream-ignored, returned `5CP_Kenya / trialDbId 1911`, ReferenceDataCache cached under the wrong key, `getTrials([7526])` returned undefined. After: dialect translates to `?trialDbId=7526`, returns the correct row, FK enrichment lands.
- **CassavaBase observation count probe** no longer triggers a full table scan that times out. Before: `GET /observations?studyDbIds=X` ≥ 180 s (timeout × 4 retries). After: `GET /observations?studyDbId=X` ~15 s, no retry.
- **CassavaBase observation-unit count probe** same shape, same fix — singular `studyDbId` honored, plural ignored.
- **`get_study` response time** on cassavabase drops from ~2 min worst-case (the screenshot) to <20 s when companion endpoints are responsive, or <10 s with a single "probe failed" warning when they aren't. Companion failures no longer pin the main response.
- **Tests** — `BrapiClient` gains 5 new specs covering dialect translation, warnings sink, all-dropped throw, raw_get passthrough, and retry override. `ReferenceDataCache` gains 3 new specs covering dialect threading, all-dropped graceful degradation, and non-dialect error propagation. `get_study` gains a cassavabase-shape end-to-end spec verifying every companion call reaches the wire as a singular key.
