Repository navigation
Bolt: inbound temporal PackStream structs (Date/Time/DateTime/…) not decoded → datetime query parameters silently dropped #4905
Description
Summary
ArcadeDB's Bolt plugin does not decode incoming temporal PackStream structures (Date, Time, LocalTime, LocalDateTime, DateTime, DateTimeZoneId, Duration). When a Bolt client sends a native temporal value as a query parameter, PackStreamReader.readStructure() returns it as an opaque StructureValue (raw signature + fields) instead of a java.time value. The parameter therefore reaches the query engine as a meaningless object and is effectively dropped: a datetime-typed parameter can never bind, match, or be stored correctly.
The output/read direction works (properties are streamed back to the client, BoltStructureMapper renders temporals as ISO-8601 strings), so the gap is input-only and easy to miss.
Credit
Diagnosed by @agc-63 (Angel Garcia) while running the community ArcadeDB driver for getzep/graphiti end-to-end against a real ArcadeDB 26.7.1 instance. See the detailed write-up on getzep/graphiti#1310:
getzep/graphiti#1310 (comment)
He confirmed it by reading PackStreamReader.java directly and worked around it at the neo4j Python driver level (monkeypatching dehydrate_datetime to emit ISO strings). The proper fix belongs here in ArcadeDB.
Root cause
bolt/src/main/java/com/arcadedb/bolt/packstream/PackStreamReader.java — readStructure() collects the struct fields and returns a generic StructureValue; there is no branch that maps the Bolt temporal signatures back to java.time types. BoltStructureMapper only implements the reverse (Java → Bolt, as strings) direction.
Note ArcadeDB negotiates Bolt v4.4 max (BoltNetworkExecutor.SUPPORTED_VERSIONS = { v4.4, v4.0, v3.0 }), so clients use the legacy (pre-5.0) DateTime/DateTimeZoneId encoding where the seconds field is the local epoch-second (offset folded in), not the UTC epoch-second. The decoder must use the legacy semantics (and may defensively also handle the 5.0 UTC signatures 0x49/0x69).
The decode must happen on the parameter path, not in the generic readStructure(): the top-level Bolt ROUTE message signature (0x66) collides with the legacy DateTimeZoneId signature (0x66, 'f'), and message envelopes flow through the same reader. Temporal decoding is therefore applied when hydrating the RUN parameters map (where a 0x66 struct is unambiguously a temporal), leaving message parsing untouched.
Scope: this is 1 of 2 fixes for full parity
Decoding the wire is necessary but not sufficient for a query like WHERE e.valid_at <= $reference_time to work like Neo4j/FalkorDB. The Cypher engine also needs a follow-up so a native temporal parameter participates in temporal comparison:
ast/ParameterExpression.evaluate()returns the parameter value raw (no temporal normalization).ast/ComparisonExpressiononly takes the temporal-comparison branch when both operands areCypherTemporalValue; there is nojava.util.Date/java.timeComparablefallback.ast/PropertyAccessExpression.convertFromStorage()wrapsLocalDate/LocalDateTime(and parseable strings) intoCypherTemporalValue, but notjava.util.DateorZonedDateTime.
Follow-up (separate change): normalize java.time/java.util.Date parameters into CypherTemporalValue for comparison, and extend convertFromStorage() to wrap java.util.Date/ZonedDateTime. That closes the loop so no client-side datetime(...) wrapping is needed.
Fix (this issue)
Decode inbound Bolt temporal structs into java.time types on the parameter path, mirroring the signatures BoltStructureMapper already emits outbound:
| Bolt struct | Signature | Decoded to |
|---|---|---|
| Date | 0x44 'D' |
LocalDate |
| Time | 0x54 'T' |
OffsetTime |
| LocalTime | 0x74 't' |
LocalTime |
| LocalDateTime | 0x64 'd' |
LocalDateTime |
| DateTime (legacy) | 0x46 'F' |
OffsetDateTime |
| DateTimeZoneId (legacy) | 0x66 'f' |
ZonedDateTime |
| DateTime (UTC, 5.0+) | 0x49 'I' |
OffsetDateTime |
| DateTimeZoneId (UTC, 5.0+) | 0x69 'i' |
ZonedDateTime |
These map onto ArcadeDB's DATETIME/DATE native types (Type.TYPES_BY_USERTYPE), so they are storable and comparable once the engine follow-up lands.
Test plan
- Bolt-module unit regression test: round-trip each temporal struct through
PackStreamWriter→PackStreamReader→ decoder and assert the correctjava.timevalue, plus nested-in-parameter-map hydration. - Follow-up: engine-level test that
WHERE valid_at <= $parammatches with a native temporal parameter.
Activity
lvca commentedon Jul 3, 2026
Both parts implemented under this issue (branch pending review, not yet committed).
Part 1 - Bolt wire decode (
boltmodule)BoltStructureMapper.fromPackStreamValue()- recursively decodes inbound temporal PackStream structs intojava.timevalues, handling the legacy (Bolt <=4.4)Date/Time/LocalTime/LocalDateTime/DateTime/DateTimeZoneIdencoding plus the 5.0 UTC signatures defensively.BoltMessage.parseRun()- hydrates the RUN parameters map through it. Done on the parameter path (not the genericreadStructure()) to avoid theROUTEmessage vs legacyDateTimeZoneId0x66signature collision.PackStreamTemporalInputTest(11 tests, wire round-trip per type + nested-in-parameter-map).Part 2 - engine temporal normalization (
enginemodule) - needed so a native temporal parameter actually compares against a stored temporal without anydatetime(...)wrapping:TemporalUtil.fromCoreJavaType()- inverse oftoCoreJavaType(); wraps nativejava.time/java.util.Datevalues into theirCypherTemporalValuetype (idempotent, non-temporals passed through).ComparisonExpression- coerces both operands viafromCoreJavaType()before the temporal-comparison branch, so a native temporal parameter compares against a stored temporal instead of silently not matching. The storage/write path is untouched (it still receives raw java values), soSETserialization is unaffected.PropertyAccessExpression.convertFromStorage()- generalized to wrap all native temporals (incl.java.util.Date, the defaultDATETIMEstorage type, andZonedDateTime), not justLocalDate/LocalDateTime.OpenCypherTemporalParameterTest(3 tests) reproduces the graphitiWHERE e.valid_at <= $reference_timescenario with nativeZonedDateTime/Date/OffsetDateTime/Instantparameters, over both native-datetime and ISO-string stored values.Verification: full
boltsuite (223 tests) and fullcom.arcadedb.query.opencypher.**suite (6636 tests) pass, 0 failures/errors.Net effect for the getzep/graphiti driver: once on a build with these fixes, both the
neo4j-driverdehydrate_datetimemonkeypatch and thedatetime(...)wrapping ingraph_data_operations.pycan be dropped - plainWHERE e.valid_at <= $reference_timeworks like the Neo4j/FalkorDB path.review(#4905): guard temporal coercion on hot path, harden malformed …
review(#4905): drop dead Calendar branch, memoize constant temporal p…
fix(#4905): decode inbound Bolt temporal params and normalize native …
review(#4907): real-driver offset coverage, drop unused getter, docum…
review(#4907): cover node-element property path in IT, document Insta…
fix(#4907): serialize outbound Bolt temporals as native PackStream st…
fix(#4922): register OffsetDateTime as a storable DATETIME type (#4923)
fix(#4905): decode inbound Bolt temporal params and normalize native …
fix(#4907): serialize outbound Bolt temporals as native PackStream st…
fix(#4922): register OffsetDateTime as a storable DATETIME type (#4923)