Release 1.11.0 — a failed query no longer looks like an empty one
A failed query no longer looks like an empty one
run_query returns count: -1 when the query did not run — Owlery errored, or the retries were exhausted. It is not a "too many to count" sentinel, and it is unrelated to the preview -1 in get_term_info. A 400 Bad Request returns count: 0, so zero really is zero.
To a model, count: 0, rows: [] and count: -1, rows: [] differ by one character and mean opposite things: no matching rows versus we have no idea. Worse, VFBquery reports the failure over HTTP 200, so the cache in front of it stores that failure as a valid answer for CACHE_STALE_TIME — six months. One transient Owlery blip could be served as a confident "there are none" for half a year.
count_status
Every run_query result now carries count_status:
| value | meaning |
|---|---|
exact |
the upstream count is a real total |
row_count |
no total was supplied; the count is the number of rows returned |
unavailable |
the query failed — the count is meaningless |
The raw count is passed through unaltered, so any consumer already guarding on count >= 0 behaves exactly as before. count_status is what new consumers should read.
A failed result also gains an unmissable _note stating in plain terms that this is not an empty result and must not be reported as "0", "none" or "no results".
Two shaping bugs fixed
- The paging note was gated on arithmetic that
-1fails. It was emitted only whentotal > offset + returned; for a failure that is-1 > 25, which is false. The one result that most needed an explanation was the one that got none. There is now no paging arithmetic at all on a failed result, so nothing ever renders "of -1". - The payload was rebuilt from a fixed key list, silently dropping
capped,truncated,warningsand anything VFBquery adds in future — precisely the flags that qualify how much of a result you are actually looking at. Shaping now destructures-and-spreads, so unknown top-level keys survive. Scalars and_notestay ordered ahead of the bulk rows; a caveat printed after 25 rows of data is a caveat that gets skimmed past.
force_refresh
get_term_info and run_query both accept an optional force_refresh, which sets X-Force-Refresh on the upstream call. run_query also retries a -1 once, automatically — re-running the identical call would only replay the cached failure.
Important
Deployment prerequisite. nginx honours X-Force-Refresh only from a whitelisted client IP. Verified from outside the whitelist: a forced GET still returns x-cache-status: HIT — the header is silently ignored. Unless this server's egress IP is on v3-cached's whitelist, force_refresh and the automatic retry are no-ops. They are harmless when ignored, and the annotation half of this release works regardless, but the recovery half does not. A forced call that comes back as a cache HIT now logs a warning, so this is visible rather than mysterious.
Tests
Shaping moved to src/runQueryShape.ts so it can be tested at all — importing dist/index.js starts a server as a side effect. npm test now compiles and runs 15 tests covering the failure cases, key survival, key ordering, paging notes, image stripping and a full serialise round trip.
README.md, TECHNICAL.md and LLM_GUIDANCE.md are updated.
Compatibility
Minor rather than patch: this adds a tool parameter and a result field. Both are additive — no existing field changed meaning or shape.
Full changelog: v1.10.0...v1.11.0