fix: serialise OffsetDateTime as RFC 3339 - #609
dudanogueira wants to merge 2 commits into
Conversation
OffsetDateTime.toString() omits the seconds when the second and the nanosecond are both zero, so a timestamp on an exact minute boundary went on the wire as "2024-03-01T00:00Z". RFC 3339's partial-time requires hour:minute:second, and the server rejects the short form -- breaking writes and filters alike for most timestamps written by hand. DateUtil owned the read side but had no write-side counterpart, which is why the same toString() was copy-pasted into all six marshalling sites. It now has toRFC3339(), which always writes the seconds and keeps the fraction variable-width so sub-second precision is neither invented nor truncated. The array and list variants in Filter and InsertManyRequest were affected too, not just the three scalar sites in the report. Reading is unchanged and stays lenient: OffsetDateTime.parse accepts both forms, so timestamps written by older clients still load. Closes #605 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Infrastructure as Code | View in Orca | ||
| SAST | View in Orca | ||
| Secrets | View in Orca | ||
| Vulnerabilities | View in Orca |
There was a problem hiding this comment.
Pull request overview
This PR fixes OffsetDateTime wire serialization to always produce RFC 3339-compliant timestamps (notably always including seconds), preventing server-side rejections for minute-boundary values like 2024-03-01T00:00:00Z.
Changes:
- Add
DateUtil.toRFC3339(OffsetDateTime)backed by a customDateTimeFormatterBuilderformatter that always prints seconds while preserving sub-second precision. - Update all affected gRPC/REST marshalling sites (filters and batch insert shapes) to use
DateUtil.toRFC3339(...)instead ofOffsetDateTime.toString(). - Add/adjust unit + integration tests to cover minute-boundary timestamps and ensure round-tripping/parsing behavior remains compatible.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/io/weaviate/client6/v1/internal/DateUtil.java | Introduces RFC 3339 formatter + toRFC3339 and updates Gson adapter write path. |
| src/main/java/io/weaviate/client6/v1/api/collections/query/Filter.java | Uses DateUtil.toRFC3339 for date filter operands (scalar + array). |
| src/main/java/io/weaviate/client6/v1/api/collections/data/InsertManyRequest.java | Uses DateUtil.toRFC3339 for OffsetDateTime marshalling (scalar, list, array). |
| src/test/java/io/weaviate/client6/v1/internal/Rfc3339DateTest.java | New focused unit tests for formatting, round-trip parsing, and marshalling at all relevant call sites. |
| src/test/java/io/weaviate/client6/v1/internal/json/JSONTest.java | Adds JSON serialization expectations for OffsetDateTime including minute-boundary and fraction/offset cases. |
| src/it/java/io/weaviate/integration/SearchITest.java | Adjusts integration test to use minute-boundary timestamps to guard the regression in filters. |
| src/it/java/io/weaviate/integration/DataITest.java | Adjusts integration test to use minute-boundary timestamps to guard the regression in writes/round-trips. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| .appendValue(ChronoField.SECOND_OF_MINUTE, 2) | ||
| .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) | ||
| .appendOffsetId() | ||
| .toFormatter(); |
Raised in review on #609: the formatter is built with toFormatter(), which takes the default locale, so in a locale using non-Latin digits it could emit a timestamp that is not RFC 3339. The locale is not what chooses the digits. DecimalStyle is, and a builder always starts from DecimalStyle.STANDARD -- measured on JDK 25, DecimalStyle.of(Locale.forLanguageTag("hi-IN-u-nu-deva")) is DecimalStyle[०+-.] and formatting through it does give २०२४-०३-०१T००:००:००Z, but neither toFormatter() nor withLocale(...) reaches that. Only an explicit withDecimalStyle(...) does, and nothing calls it. So there is nothing to fix, and this is a guard rather than a fix: it fails if someone later gives the formatter a locale-derived decimal style, which is the only route to the reported problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNeKV7TAYZGDyT9agqJwUf
|
Looked into the The locale is not what selects the digits. So the mechanism the comment describes is real, but nothing here reaches it: only an explicit Rather than a no-op change I have added the invariant as a test ( which is the only route to the problem as described. Unit suite green: 419 tests. |
Motivation
OffsetDateTime.toString()drops the seconds when the second and the nanosecond are both zero, so2024-03-01T00:00:00Zgoes on the wire as2024-03-01T00:00Z. RFC 3339'spartial-timerequireshour ":" minute ":" second, and Weaviate rejects the short form:Every timestamp on an exact minute boundary is affected — which is most timestamps written by hand — and it breaks writes and filters alike.
toString()is a display format, not a wire format.Approach
DateUtilalready owned the read side (fromISO8601) but had no write-side counterpart, which is exactly why the sametoString()call got copy-pasted into every marshalling site. It now hastoRFC3339(), and all six sites call it.The formatter is built with
DateTimeFormatterBuilderrather thanofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"): the pattern form would fix the reported bug while silently truncating sub-second precision that works today.appendFraction(NANO_OF_SECOND, 0, 9, true)keeps the fraction variable-width, so it is neither invented for whole seconds nor truncated for nanos.Scope: six call sites, not three
The issue named three. The list and array variants have the same bug:
Filter.DateOperand.appendToFilter.DateArrayOperand.formattedcontainsAny/containsNoneInsertManyRequestscalarInsertManyRequestList<OffsetDateTime>InsertManyRequestOffsetDateTime[]DateUtilGsonTypeAdapterAggregate filters, boost filters and
deleteManyall funnel throughFilter, so they are fixed without separate changes.Filter.DateOperand.toString()is left alone — it feeds the human-readableFilter.toString(), not the wire.Key areas for review
DateUtil.RFC3339— the formatter itself; the fraction handling is the part worth a second lookOffsetDateTime.parse, whose default formatter treats seconds and fraction as optional, so it already accepts both spellings and data written by older clients still loads. This is what makes the change safe to ship without a migration.Testing
The reason no existing test caught this: every date test seeded from
OffsetDateTime.now(), which practically never has second and nano zero. The new tests use literals.Rfc3339DateTest— 26 cases: formatting across minute-boundary/millis/nanos/non-UTC/negative offsets, round-trip through the reader, the old truncated form still parsing, all eight filter comparison paths (includingcreatedAt()/lastUpdatedAt(), which takeOffsetDateTimeonly and so had noStringworkaround), the array operand, and the threeInsertManyRequestshapes.JSONTest— fiveOffsetDateTimerows; the file had none. Each row asserts both directions.DataITest.testDataTypesandSearchITest.test_filterCreateUpdateTimenow use minute-boundary values, so the existing round-trip and filter assertions guard the regression against a real server.Verified the tests fail without the fix — with
src/mainreverted, theJSONTestrows fail withexpected:<"2024-03-01T00:00[:00]Z"> but was:<"2024-03-01T00:00[]Z">.Locally green: 418 unit tests, and
DataITest+SearchITest+OrmITestagainst a container (48 run, 3 skipped by version gates).Breaking changes
None. Public API is unchanged;
DateUtil.toRFC3339is additive, and the wire format only becomes more standards-compliant.Closes #605
🤖 Generated with Claude Code
https://claude.ai/code/session_01WmY5dAGWCccWDoqkKNC2JU