Skip to content

fix(arcadedb): work around index-scan datetime comparison bug (3 code paths)#3

Closed
agc-63 wants to merge 1 commit into
ArcadeData:feat/arcadedb-backendfrom
agc-63:fix/arcadedb-retrieve-episodes-datetime
Closed

fix(arcadedb): work around index-scan datetime comparison bug (3 code paths)#3
agc-63 wants to merge 1 commit into
ArcadeData:feat/arcadedb-backendfrom
agc-63:fix/arcadedb-retrieve-episodes-datetime

Conversation

@agc-63

@agc-63 agc-63 commented Jul 5, 2026

Copy link
Copy Markdown

What

While digging into the search_interface PR (#2 — thanks again for the quick review on that one!), I hit a ClassCastException from ArcadeDB when running queries that compare datetime properties on indexed fields. Turned out to be an engine-level bug, so I've filed arcadedb#5008 upstream with what I found, plus a suggested fix.

In the meantime, this PR works around it on the driver side in three places that do unwrapped datetime comparisons in generated Cypher:

  • graphiti_core/utils/maintenance/graph_data_operations.pyretrieve_episodes()'s valid_at filter/order
  • graphiti_core/search/search_filters.pydate_filter_query_constructor(), used by the valid_at/invalid_at/created_at/expired_at edge filters
  • graphiti_core/graphiti.pysummarize_saga() and _saga_get_previous_episode_uuid()'s created_at/valid_at filters and ordering

The fix wraps both sides of the comparison in Cypher's datetime(...), gated on provider == GraphProvider.ARCADEDB, following the same pattern already used for the Neptune-specific branches elsewhere in the codebase. So this only changes behavior for the ArcadeDB provider — no other backend is touched.

Why it wasn't caught earlier

The bug only triggers when a range index exists on the compared property — ArcadeDB's query planner picks a different (buggy) execution path in that case. My earlier testing didn't build out the full index set, so this slipped through. Once I rebuilt against the real 28-index set from build_indices_and_constraints(), it reproduced consistently.

Worth calling out: the search_filters.py case doesn't throw — it silently returns empty results instead, which took a bit longer to track down.

Testing

Verified against a real ArcadeDB 26.8.1-SNAPSHOT instance (Docker), reproducing each of the 3 code paths 3x before the fix and confirming correct behavior 3x after, using the full index set rather than an isolated one.

Upstream

Filed arcadedb#5008 with the root cause (NodeIndexRangeScan.compareValues() has explicit numeric-type coercion but no equivalent for temporal types) and a suggested fix (reusing TemporalUtil.fromCoreJavaType(), which the ComparisonExpression path already uses). Happy to have this driver-side workaround removed once/if that lands — just flagging it's a real engine gap, not something we need to carry long-term.

…de paths

ArcadeDB's Cypher index-range-scan optimizer (NodeIndexRangeScan) throws
a ClassCastException when comparing a stored LocalDateTime against a
native Bolt OffsetDateTime parameter, because it has explicit numeric
coercion but no equivalent for temporal types. This only triggers when
a range index exists on the compared property, so it wasn't caught by
earlier testing that didn't build the full index set.

This affects three code paths that compare datetime properties without
wrapping them in Cypher's datetime():

- graph_data_operations.py: retrieve_episodes() valid_at filter/order
- search_filters.py: date_filter_query_constructor() used by
  valid_at/invalid_at/created_at/expired_at edge filters
- graphiti.py: summarize_saga() and _saga_get_previous_episode_uuid()
  created_at/valid_at filters and ordering

Work around by wrapping both sides of the comparison in datetime(...)
in the generated Cypher, gated on provider == GraphProvider.ARCADEDB,
mirroring the existing Neptune-only branch pattern already used
elsewhere in the codebase. The search_filters.py case was silently
returning empty results rather than crashing, which is arguably worse.

Filed arcadedb#5008 upstream with a suggested engine-level fix
(reusing TemporalUtil.fromCoreJavaType(), already used by the
ComparisonExpression path, in NodeIndexRangeScan.compareValues()).
Verified against a real ArcadeDB 26.8.1-SNAPSHOT instance with the
full 28-index set from build_indices_and_constraints(), 3x
before/after for each of the 3 code paths.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces workarounds for ArcadeDB's native datetime comparison issues by wrapping datetime properties and parameters in datetime(...) across several queries in graphiti.py, search_filters.py, and graph_data_operations.py. The review feedback highlights that wrapping these indexed properties makes the queries non-sargable, which could lead to performance degradation on larger datasets. Additionally, a type mismatch was identified in graph_data_operations.py where a dynamically constructed query using f-strings is incorrectly annotated as LiteralString instead of str.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@@ -138,9 +157,9 @@ async def retrieve_episodes(
query_params['source'] = source.name

query: LiteralString = (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The query variable is annotated as LiteralString, but it is constructed dynamically using f-strings and query_filter (which is also a dynamic string). In Python, f-strings with runtime variables do not satisfy the LiteralString type constraint, which will cause static type checkers like mypy or pyright to report a type mismatch error. We should change the type annotation to str.

Suggested change
query: LiteralString = (
query: str = (

Comment on lines +120 to +125
if provider == GraphProvider.ARCADEDB and operator not in (
ComparisonOperator.is_null,
ComparisonOperator.is_not_null,
):
value_name = f'datetime({value_name})'
param_name = f'datetime({param_name})'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Wrapping the indexed property (value_name) in the datetime() function makes the query non-sargable. This means ArcadeDB's query planner will not be able to utilize the range index on these properties, potentially falling back to full scans and causing performance degradation on larger datasets. While this is a necessary workaround for the upstream engine bug (arcadedb#5008), please ensure there is a tracking issue or TODO to remove this workaround once the upstream fix is released.

@agc-63

agc-63 commented Jul 5, 2026

Copy link
Copy Markdown
Author

Closing this in favor of the upstream engine fix — arcadedb#5008 was fixed in main (commit ca39750aa), shipping in 26.7.2, so the driver-side datetime(...) workaround here is no longer needed. Thanks again for the quick turnaround, @lvca!

@agc-63 agc-63 closed this Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant