Repository navigation
fix(api-model): DataWrapper.items is a List, so a caller can read it by position - #141
Merged
Merged
Conversation
…by position
`items` is an ordered JSON array on the wire, in the order the caller asked for —
`sortBy` for a read, insertion order for a bulk write — but it was typed as
`Collection<T>`, which throws that away. Every caller reading a single result had
to write
client.timeseries().retrieve(request).getItems().iterator().next()
because `Collection` has no `get(int)`. This is not hypothetical: the SDK docs
described `getItems().get(0)` on 27 pages and none of them compiled, and the
Python client says `retrieve_datapoints(rf)[0]` for the same read. The envelope
was the only reason Java looked worse than the language it wraps.
The getter narrows to `List<T>`; the setter still takes any `Collection`, so the
graph endpoints that hand over a `Set` keep compiling. Three behaviours are held
fixed on purpose, and `DataWrapperItemsListTest` pins each:
- a list is held as given, not copied, so a caller mutating the list it passed
still sees the envelope change;
- a non-list is copied and keeps that collection's iteration order;
- `null` is still stored as `null` — several request validators test
`getItems() == null` to reject a body that sent `"items": null`, and an absent
array must not arrive as an empty one.
The wire shape does not change in either direction. `Iterable<T>` was considered
instead and rejected: it does not help read the first of one, and an envelope
carrying `nextCursor` that answers a for-each with a single page invites a silent
paging bug.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
olavgg
approved these changes
Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
DataWrapper.itemsis an ordered JSON array on the wire, in the order the caller asked for —sortByfor a read, insertion order for a bulk write. It was typed asCollection<T>, whichthrows that ordering away at the type level, so there is no way to address a result by position.
Reading the first of one result therefore had to be written:
This turned up as a concrete cost rather than a style opinion. Fixing the SDK documentation
(IntelliStream-DataHub/datahub-sdk-docs#103) found 27 examples across 26 pages that all said
getItems().get(0), and none of them had ever compiled:The Python client expresses the same read as
retrieve_datapoints(rf)[0].get_datapoints(). Theenvelope's type was the only reason the Java tab of every one of those pages had to be uglier
than the client it documents — and
iterator().next()is also worse on failure, throwing abare
NoSuchElementExceptionwith no message when the result is empty.What changes
The getter narrows to
List<T>. The setter still accepts anyCollection, so the graphendpoints — whose nodes and relations are sets — keep compiling untouched.
Three behaviours are held fixed deliberately, each pinned by a test in
DataWrapperItemsListTest:SetsnullstaysnullgetItems() == nullto reject a body that sent"items": null; an absent array must not arrive as an empty oneThe wire shape is unchanged in both directions — serialization and deserialization round-trip
identically, including
nextCursor.Why not
Iterable<T>Considered first, since it is two lines, and measured: Jackson does not collapse the envelope
into a bare array (bean properties win over add-on
Iterabledetection), so that risk is notreal. It was rejected for two other reasons:
resp.iterator().next()— character for character what is already there.DataWrappercarriesnextCursor, sofor (Event e : page)reads like"every event" and silently processes one page.
If for-each over results is wanted later, the honest form is a paging helper on the service that
follows
nextCursor, not anIterableenvelope that hides the existence of paging.Verified
compileJava+compileTestJavaacross every module: all 279getItems()/setItems()callsites compile unchanged.
:datahub-api-model:test,:datahub-java-sdk:test,:datahub-infra:test,:datahub-api:testall green.
DataWrapperItemsListTestfails to compile againstmain, so it is a real guard ratherthan a restatement.
docs/guides/detect-events.mdx's original Java verbatimagainst them: it fails on
mainwith the error above and compiles here.Docs follow-up
datahub-sdk-docs#103 worked around this by rewriting those 27 sites to
.iterator().next()—that is what makes the docs' compile job green today, and it should merge on its own schedule.
Once this lands, those sites can go back to
.get(0), which is both what they said originallyand what the Python and Rust tabs beside them look like. Happy to raise that revert as a separate
PR; it needs no coordination with this one, since
.iterator().next()keeps compiling either way.🤖 Generated with Claude Code