Skip to content

Opencypher tck part 3#3363

Merged
lvca merged 15 commits intomainfrom
opencypher-tck
Feb 7, 2026
Merged

Opencypher tck part 3#3363
lvca merged 15 commits intomainfrom
opencypher-tck

Conversation

@lvca
Copy link
Contributor

@lvca lvca commented Feb 7, 2026

Additional cleanup

lvca added 14 commits February 6, 2026 10:24
Reached much better compliance with the latest changes:

```
=======================================================
         OpenCypher TCK Compliance Report
=======================================================
Total scenarios:  3897
Passed:           1908 (48%)
Failed:           1939 (49%)
Skipped:          50 (1%)
-------------------------------------------------------
By category:
  clauses/call                               2/ 52 passed (3%)
  clauses/create                            64/ 78 passed (82%)
  clauses/delete                            24/ 41 passed (58%)
  clauses/match                            292/381 passed (76%)
  clauses/match-where                       25/ 34 passed (73%)
  clauses/merge                             47/ 75 passed (62%)
  clauses/remove                            29/ 33 passed (87%)
  clauses/return                            35/ 63 passed (55%)
  clauses/return-orderby                    23/ 35 passed (65%)
  clauses/return-skip-limit                 26/ 31 passed (83%)
  clauses/set                               30/ 53 passed (56%)
  clauses/union                              8/ 12 passed (66%)
  clauses/unwind                            10/ 14 passed (71%)
  clauses/with                              14/ 29 passed (48%)
  clauses/with-orderBy                     124/292 passed (42%)
  clauses/with-skip-limit                    7/  9 passed (77%)
  clauses/with-where                        10/ 19 passed (52%)
  expressions/aggregation                   23/ 35 passed (65%)
  expressions/boolean                      150/150 passed (100%)
  expressions/comparison                    36/ 72 passed (50%)
  expressions/conditional                   13/ 13 passed (100%)
  expressions/existentialSubqueries          4/ 10 passed (40%)
  expressions/graph                         32/ 61 passed (52%)
  expressions/list                         120/185 passed (64%)
  expressions/literals                     120/131 passed (91%)
  expressions/map                           28/ 44 passed (63%)
  expressions/mathematical                   3/  6 passed (50%)
  expressions/null                          44/ 44 passed (100%)
  expressions/path                           0/  7 passed (0%)
  expressions/pattern                       19/ 50 passed (38%)
  expressions/precedence                    20/121 passed (16%)
  expressions/quantifier                   478/604 passed (79%)
  expressions/string                        22/ 32 passed (68%)
  expressions/temporal                       0/1004 passed (0%)
  expressions/typeConversion                19/ 47 passed (40%)
  useCases/countingSubgraphMatches           6/ 11 passed (54%)
  useCases/triadicSelection                  1/ 19 passed (5%)
=======================================================
```
@lvca lvca added this to the 26.2.1 milestone Feb 7, 2026
@lvca lvca self-assigned this Feb 7, 2026
@lvca lvca marked this pull request as ready for review February 7, 2026 06:55
@lvca lvca merged commit 918725f into main Feb 7, 2026
7 of 9 checks passed
@lvca lvca deleted the opencypher-tck branch February 7, 2026 06:55
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @lvca, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly expands the OpenCypher implementation by introducing comprehensive support for temporal data types and functions. It enables users to work with dates, times, datetimes, and durations in a Cypher-compliant manner, including advanced parsing, property access, arithmetic operations, and various utility functions. The changes also include crucial parser improvements to ensure correct expression evaluation and seamless integration of temporal values with ArcadeDB's storage mechanisms.

Highlights

  • Comprehensive Temporal Type Support: Introduced a full suite of OpenCypher temporal types including CypherDate, CypherTime, CypherLocalDateTime, CypherDateTime, and CypherDuration. These types wrap Java's java.time API and provide Cypher-specific parsing, property access (e.g., date.year), and comparison logic.
  • Temporal Functions and Arithmetic: Added extensive support for Cypher temporal functions, including constructors (date(), time(), duration()), truncation functions (date.truncate(), time.truncate()), epoch conversion functions (datetime.fromepoch()), and duration calculation functions (duration.between()). Arithmetic operations (+, -, *, /) now correctly handle temporal values and durations.
  • Enhanced Expression Evaluation: Improved the expression evaluator to correctly handle list concatenation and appending with the + operator. The toInteger(), toFloat(), toBoolean(), toString(), nodes(), and relationships() functions have been refined for better type handling and Cypher compliance, including stricter type error reporting and null handling.
  • Robust Cypher Parser Logic: Refactored the Cypher expression parser to use a 'spine walk' approach for identifying top-level comparison, arithmetic, and postfix expressions. This ensures correct operator precedence and avoids misinterpreting expressions within parentheses, leading to more accurate parsing of complex queries.
  • Temporal Data Storage Integration: Implemented logic to automatically convert CypherTemporalValue objects into their corresponding java.time types (or string representations for time/duration) when storing them as properties in ArcadeDB, ensuring seamless persistence of temporal data.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • engine/src/main/java/com/arcadedb/query/opencypher/ast/ArithmeticExpression.java
    • Added imports for java.time and com.arcadedb.query.opencypher.temporal packages.
    • Integrated evaluateTemporalArithmetic for handling temporal operations.
    • Adjusted integer arithmetic logic to include DIVIDE operator and removed it from useInteger condition.
    • Implemented evaluateTemporalArithmetic method to support Duration arithmetic (add, subtract, multiply, divide) and Date/Time ± Duration operations for various temporal types.
  • engine/src/main/java/com/arcadedb/query/opencypher/ast/ComparisonExpression.java
    • Imported CypherTemporalValue for temporal comparisons.
    • Added dedicated comparison logic for CypherTemporalValue instances.
    • Refined comparison rules for Boolean, String, List, and Map types to align with Cypher's type-specific comparison behavior, returning null for incompatible ordering comparisons on lists/maps.
  • engine/src/main/java/com/arcadedb/query/opencypher/ast/InExpression.java
    • Updated valuesEqual method to prioritize direct object equality, retain numeric comparison, and explicitly return false for comparisons between incompatible types, removing the previous default string comparison fallback.
  • engine/src/main/java/com/arcadedb/query/opencypher/ast/ListIndexExpression.java
    • Added imports for Document and Map.
    • Introduced support for accessing properties of Map, Result, and Document objects using bracket notation with string keys (e.g., map['key']).
    • Modified behavior for non-numeric list indices to return null instead of throwing an IllegalArgumentException.
  • engine/src/main/java/com/arcadedb/query/opencypher/ast/PropertyAccessExpression.java
    • Added imports for CypherTemporalValue, LocalDate, and LocalDateTime.
    • Implemented property access for CypherTemporalValue objects (e.g., date.year) and automatically wrapped java.time.LocalDate and java.time.LocalDateTime from storage into their Cypher temporal counterparts for property access.
  • engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherFunctionFactory.java
    • Added numerous java.time and com.arcadedb.query.opencypher.temporal imports.
    • Expanded isCypherSpecificFunction and createCypherSpecificExecutor to include a wide range of new temporal functions (constructors, truncation, epoch, duration calculations, and clock functions).
    • Improved nodes() and relationships() functions to handle null input gracefully.
    • Revised toString(), toInteger(), toFloat(), and toBoolean() functions for stricter type checking and Cypher-compliant conversions, including throwing TypeError for unsupported conversions and handling boolean to integer conversion.
    • Modified collect() function to skip null values as per OpenCypher specification.
    • Introduced new inner classes for all temporal functions (e.g., DateConstructorFunction, DurationBetweenFunction) and helper methods for temporal value wrapping and map-based adjustments.
  • engine/src/main/java/com/arcadedb/query/opencypher/executor/ExpressionEvaluator.java
    • Added logic for list concatenation and appending using the + operator.
    • Removed DIVIDE operator from the useInteger condition and added explicit DIVIDE handling for integer arithmetic, aligning with ArithmeticExpression.java changes.
  • engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/CreateStep.java
    • Added import for com.arcadedb.query.opencypher.temporal package.
    • Introduced convertTemporalForStorage helper method to transform CypherTemporalValue objects into java.time types or string representations suitable for ArcadeDB storage when setting node and edge properties.
  • engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java
    • Refactored expression parsing to use a 'spine walk' approach for identifying top-level comparison, arithmetic, and postfix expressions, ensuring correct operator precedence.
    • Added new helper methods (findTopLevelExpression8, findTopLevelExpression5, findTopLevelExpression4, findTopLevelExpression3) to precisely locate top-level operators.
    • Updated ArithmeticExpression constructor calls to remove the ctx.getText() argument.
  • engine/src/main/java/com/arcadedb/query/opencypher/temporal/CypherDate.java
    • New file: Implements the OpenCypher Date type, wrapping java.time.LocalDate.
    • Provides static methods for now(), parse() (supporting extended ISO 8601 formats), and fromMap() (for flexible date construction).
    • Implements CypherTemporalValue for property access (year, month, day, quarter, week, etc.) and comparison.
  • engine/src/main/java/com/arcadedb/query/opencypher/temporal/CypherDateTime.java
    • New file: Implements the OpenCypher DateTime type, wrapping java.time.ZonedDateTime.
    • Provides static methods for now(), parse() (handling named timezones and compact offsets), fromEpoch(), fromEpochMillis(), and fromMap() (for flexible datetime construction).
    • Implements CypherTemporalValue for property access (date, time, timezone, epoch properties) and comparison.
  • engine/src/main/java/com/arcadedb/query/opencypher/temporal/CypherDuration.java
    • New file: Implements the OpenCypher Duration type, combining calendar and clock components.
    • Provides static methods for parse() (ISO 8601 duration strings) and fromMap() (for flexible duration construction).
    • Includes methods for add(), subtract(), multiply(), and divide() durations.
    • Implements CypherTemporalValue for property access (years, months, days, hours, minutes, seconds, fractional seconds) and approximate comparison.
  • engine/src/main/java/com/arcadedb/query/opencypher/temporal/CypherLocalDateTime.java
    • New file: Implements the OpenCypher LocalDateTime type, wrapping java.time.LocalDateTime.
    • Provides static methods for now(), parse() (supporting compact ISO 8601 formats), and fromMap() (for flexible localdatetime construction).
    • Implements CypherTemporalValue for property access (date and time properties) and comparison.
  • engine/src/main/java/com/arcadedb/query/opencypher/temporal/CypherLocalTime.java
    • New file: Implements the OpenCypher LocalTime type, wrapping java.time.LocalTime.
    • Provides static methods for now(), parse() (supporting compact ISO 8601 formats), and fromMap() (for flexible localtime construction).
    • Implements CypherTemporalValue for property access (hour, minute, second, fractional seconds) and comparison.
  • engine/src/main/java/com/arcadedb/query/opencypher/temporal/CypherTemporalValue.java
    • New file: Defines a common interface for all OpenCypher temporal value types, extending Comparable and requiring a getTemporalProperty(String name) method.
  • engine/src/main/java/com/arcadedb/query/opencypher/temporal/TemporalUtil.java
    • New file: A utility class providing methods for normalizing temporal strings, parsing timezones/offsets, truncating temporal values to various units, and calculating durations between temporal values (in months, days, seconds, or full components).
  • engine/src/test/java/com/arcadedb/query/opencypher/tck/TCKResultMatcher.java
    • Added imports for CypherTemporalValue, LocalDate, and LocalDateTime.
    • Modified normalizeValue to convert CypherTemporalValue, LocalDate, and LocalDateTime objects to their string representations for consistent comparison in TCK tests.
Activity
  • This pull request represents 'part 3' of the ongoing work to enhance OpenCypher TCK compatibility, focusing on temporal data types and functions.
  • The author, lvca, has implemented significant new features and refactored existing code to align with Cypher's temporal model.
  • The changes involve adding multiple new classes and utility methods to support the new temporal functionality.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@mergify
Copy link
Contributor

mergify bot commented Feb 7, 2026

🧪 CI Insights

Here's what we observed from your CI run for 79b5e5c.

🟢 All jobs passed!

But CI Insights is watching 👀

@claude
Copy link

claude bot commented Feb 7, 2026

PR Review: OpenCypher TCK Part 3 - Temporal Support

Thank you for this comprehensive implementation of OpenCypher temporal types and functions. This is a substantial addition (2,616 additions across 18 files) that adds critical temporal functionality to ArcadeDB's OpenCypher support. Here's my detailed review:


✅ Strengths

1. Comprehensive Temporal Implementation

  • All major temporal types implemented: Date, LocalTime, Time, LocalDateTime, DateTime, Duration
  • Full support for ISO 8601 formats including extended formats (ordinal dates, week dates)
  • Proper timezone handling with both offset and named zone support
  • Property access for temporal values (e.g., date.year, time.hour)

2. Well-Structured Architecture

  • Clean separation with dedicated temporal package
  • Consistent interface via CypherTemporalValue
  • Good use of Java's java.time API as the underlying implementation
  • Helper utilities properly centralized in TemporalUtil

3. Good Code Quality

  • Proper null handling throughout
  • Immutability maintained in temporal value classes
  • Appropriate use of switch expressions (Java 21+)
  • Consistent error messages

🔍 Issues & Recommendations

CRITICAL: Potential Arithmetic Bug

Location: ArithmeticExpression.java:38

The removal of DIVIDE from the integer exclusion check may introduce a behavior change:

// Old: final boolean useInteger = ... && operator != Operator.DIVIDE && operator != Operator.POWER;
// New: final boolean useInteger = ... && operator != Operator.POWER;

Issue: Integer division now uses truncation (l / r) instead of floating-point division. While this may be intentional for OpenCypher spec compliance, it's a breaking change.

Example:

  • 5 / 2 now returns 2 (was 2.5)

Recommendation:

  • Verify this matches OpenCypher TCK expectations
  • Add explicit test cases for integer division behavior
  • Consider documenting this in commit message or comments

MEDIUM: Duration Comparison Approximation

Location: CypherDuration.java:180-188

// Approximate comparison: convert everything to nanoseconds
final long thisTotal = (months * 30 * 86400 + days * 86400 + seconds) * 1_000_000_000L + nanosAdjustment;

Issue: Duration comparison uses approximation (months = 30 days), which can lead to unexpected results.

Example:

  • P1M (1 month) vs P30D (30 days) will compare equal
  • P1M (Feb) is actually 28/29 days in reality

Recommendation:

  • Document this limitation in JavaDoc
  • Consider throwing IllegalArgumentException when comparing durations with mixed calendar/clock components
  • Follow Neo4j Cypher's behavior for compatibility

MEDIUM: Fractional Month Approximation

Location: CypherDuration.java:101

double totalDays = weeks * 7 + days + fracMonths * 30; // fractional months → approximate days

Issue: Fractional months converted to days using 30-day approximation.

Impact: duration({months: 1.5}) creates P1M15D, which may not match actual calendar behavior.

Recommendation: Document this approximation or consider alternative approach.

LOW: Negative Duration String Formatting

Location: CypherDuration.java:228-248

The appendSecondsWithNanos method handles negative seconds with positive nanos, but this edge case may be confusing.

Example: -2s + 1ms = -1.999s

Recommendation: Add unit tests specifically for negative durations to ensure correct behavior.

LOW: Division by Zero Handling Inconsistency

Locations:

  • ArithmeticExpression.java:48: Returns null for r != 0 ? l / r : null
  • CypherDuration.java:147: Throws ArithmeticException

Recommendation: Standardize zero-division behavior across the codebase. OpenCypher spec should guide this.

LOW: Type Coercion in Helper Methods

Location: CypherDate.java:196-212

The toInt, toLong, toDouble methods use toString() for non-Number types, which could fail with NumberFormatException.

Recommendation: Add input validation or wrap in try-catch with better error messages.


🧪 Testing Concerns

Missing Test Evidence

  • No new test files added in this PR (only TCKResultMatcher.java modified)
  • With 2,616 lines of new code, comprehensive test coverage is essential

Recommendation:

  1. Add unit tests for each temporal type (CypherDateTest, CypherDurationTest, etc.)
  2. Test temporal arithmetic edge cases
  3. Test timezone conversion and DST handling
  4. Test negative values and edge cases
  5. Verify against OpenCypher TCK temporal tests

Suggested Test Cases

// Duration arithmetic
RETURN duration({days: 1}) + duration({hours: 2})

// Temporal arithmetic with ArcadeDB LocalDate storage
CREATE (n:Event {date: date('2024-01-15')})
RETURN n.date + duration({months: 1})

// Negative durations
RETURN duration({seconds: -5}) * 2

// Zero division
RETURN duration({days: 1}) / 0

// Timezone conversions
RETURN datetime({timezone: 'Europe/Stockholm'})

// Property access
RETURN date('2024-01-15').quarter

🎯 Code Style & Best Practices

Follows Project Conventions

  • No fully qualified names (imports used correctly)
  • Minimal curly braces for single-statement ifs
  • Proper use of final keyword
  • No System.out debug statements

Good Documentation

  • Clear JavaDoc comments on classes
  • Inline comments for complex logic

⚠️ Minor Style Issues

  • Some long methods could be refactored (e.g., CypherDateTime.fromMap - 80 lines)
  • Consider extracting magic numbers to constants (e.g., 1_000_000_000L, 86400)

🔒 Security Considerations

No Security Issues Found

  • No SQL injection vectors
  • No command execution
  • Proper input validation in parsers
  • No sensitive data exposure

⚡ Performance Considerations

Generally Good

  • Uses primitive types where possible
  • Efficient use of Java's java.time API
  • No unnecessary object allocations

⚠️ Minor Concerns

  • String concatenation in toString() methods uses StringBuilder (good)
  • Regex pattern compilation in TemporalUtil is static (good)
  • CypherDuration.parse() compiles pattern on every call - consider caching

📋 Action Items

Before Merging

  1. ✅ Verify integer division behavior matches OpenCypher spec
  2. ⚠️ Add comprehensive unit tests for temporal types
  3. ⚠️ Test against OpenCypher TCK temporal test suite
  4. ⚠️ Document duration comparison approximation
  5. ✅ Run full test suite to ensure no regressions

Nice to Have

  • Extract magic numbers to named constants
  • Add performance benchmarks for temporal operations
  • Consider refactoring long methods (fromMap implementations)

🎉 Summary

This is a high-quality implementation of OpenCypher temporal support. The code is well-structured, follows project conventions, and provides comprehensive functionality. The main concerns are:

  1. Test coverage - Critical to verify correctness
  2. Integer division behavior change - Needs verification
  3. Duration comparison approximation - Should be documented

With proper testing and minor documentation improvements, this will be an excellent addition to ArcadeDB.

Recommendation: ✅ Approve with minor changes (add tests and verify integer division behavior)


Review performed using ArcadeDB's CLAUDE.md guidelines and best practices for OpenCypher implementations.

@claude
Copy link

claude bot commented Feb 7, 2026

Pull Request Review: OpenCypher TCK Part 3

This is an impressive implementation that significantly improves OpenCypher compliance (from ~41% to ~48%). The PR introduces comprehensive temporal support and fixes critical issues with operator precedence and type comparisons. Here's my detailed review:

Strengths

1. Comprehensive Temporal Implementation

  • Excellent architecture with CypherTemporalValue interface providing a clean abstraction
  • All 7 new temporal classes (CypherDate, CypherTime, CypherLocalTime, CypherLocalDateTime, CypherDateTime, CypherDuration, TemporalUtil) are well-structured
  • Strong support for ISO 8601 parsing including week dates, ordinal dates, and compact formats
  • Proper handling of fractional components with cascading (e.g., 1.5 years → 1 year + 6 months)

2. Robust Type System

  • ComparisonExpression.java: Excellent refactoring that properly handles type-specific comparisons
  • Temporal, boolean, string, list, and map comparisons now follow OpenCypher semantics correctly
  • Proper three-valued logic with null propagation

3. Arithmetic Improvements

  • ArithmeticExpression.java: Added temporal arithmetic (date/time ± duration, duration operations)
  • Fixed integer division to properly return integers instead of always using doubles
  • Good separation of temporal vs numeric arithmetic

4. Function Coverage

  • Added 25+ temporal functions (constructors, truncation, epoch, duration calculations)
  • collect() now correctly skips nulls per OpenCypher spec
  • Type conversion functions (toString, toInteger, toFloat, toBoolean) now properly throw errors for unsupported types

🐛 Issues & Concerns

1. Division by Zero Handling Inconsistency ⚠️

Location: ArithmeticExpression.java:48

case DIVIDE -> r != 0 ? l / r : null; // Integer division (truncation)
case MODULO -> r != 0 ? l % r : null;

Issue: Integer division returns null on division by zero, but later for doubles (line 165) you'll get Infinity or NaN from Java's standard behavior. OpenCypher spec typically requires consistent error behavior.

Recommendation: Consider throwing ArithmeticException for division by zero to match the behavior in CypherDuration.divide() (line 147) which throws an exception.


2. Potential Overflow in Duration Comparison ⚠️

Location: CypherDuration.java:183-184

final long thisTotal = (months * 30 * 86400 + days * 86400 + seconds) * 1_000_000_000L + nanosAdjustment;
final long otherTotal = (d.months * 30 * 86400 + d.days * 86400 + d.seconds) * 1_000_000_000L + d.nanosAdjustment;

Issue: This calculation can overflow for large durations. For example, a duration of 1000 years:

  • 1000 * 12 months * 30 days * 86400 seconds * 1_000_000_000 nanoseconds = 3.11e19 (exceeds Long.MAX_VALUE = 9.22e18)

Recommendation: Use BigInteger for this comparison or implement a component-by-component comparison to avoid overflow.


3. Temporal Arithmetic Missing SUBTRACT Operation 🔍

Location: ArithmeticExpression.java:79-133

Observation: You handle date + duration but not date - dateduration, which is part of the OpenCypher spec.

Recommendation: Add support for temporal subtraction to produce durations:

// Date - Date → Duration
if (operator == Operator.SUBTRACT && leftValue instanceof CypherTemporalValue && rightValue instanceof CypherTemporalValue)
  return TemporalUtil.durationBetween((CypherTemporalValue) rightValue, (CypherTemporalValue) leftValue);

4. Month-to-Days Approximation Issues ⚠️

Location: Multiple places using months * 30

Issue: Several locations approximate months as 30 days:

  • CypherDuration.java:101: fracMonths * 30
  • CypherDuration.java:183: months * 30 * 86400

Problem: This breaks calendar accuracy. P1M (1 month) should be context-dependent:

  • Jan 1 + 1 month = Feb 1 (31 days)
  • Feb 1 + 1 month = Mar 1 (28/29 days)

Recommendation: Document this limitation clearly, or refactor duration comparison to avoid requiring a total ordering (OpenCypher allows durations to be incomparable in some cases).


5. Missing Null Checks in Function Implementations 🔍

Location: CypherFunctionFactory.java, various functions

Example: DateConstructorFunction.execute() (line 667) doesn't check if args[0] is null before type-checking.

Issue: While many functions now check for null, several temporal functions could throw NullPointerException if passed null arguments.

Recommendation: Add null guards at the start of all temporal constructor functions:

if (args.length > 0 && args[0] == null)
  return null;

6. Negative Duration String Formatting Edge Case 🐛

Location: CypherDuration.java:236-243

} else if (s <= 0 && nanos > 0 && s != 0) {
  // Negative seconds + positive nanos: e.g., -2s + 1ms = -1.999s
  final long adjSec = s + 1;
  final int adjNanos = 1_000_000_000 - nanos;

Issue: The logic handles -2s + 1ms but the normalization in the constructor (line 49-51) should already ensure nanos are always positive. The edge case handling might be unreachable or indicates a normalization bug.

Recommendation: Add unit tests for negative durations to verify correct normalization and string formatting. Consider adding assertions to verify invariants.


7. Code Style Inconsistency ℹ️

Location: InExpression.java:292-297

if (a == null && b == null)
  return true;
if (a == null || b == null)
  return false;

Issue: The CLAUDE.md guidelines state: "if statements with only one child sub-statement don't require a curly brace open/close", but the existing codebase is inconsistent. This change removed braces, which is fine, but ensure consistency across the PR.


🧪 Testing Concerns

Missing Unit Tests

Per CLAUDE.md: "all new server-side code must be tested with a test case"

Critical areas lacking explicit unit tests:

  1. Temporal arithmetic: No dedicated test for date + duration, duration * number, etc.
  2. Duration parsing: Edge cases like PT0S, P0D, negative durations
  3. Overflow handling: Large year values, very long durations
  4. Null propagation: Temporal functions with null arguments
  5. Type errors: Temporal comparisons between incompatible types

Recommendation: Create CypherTemporalTest.java with comprehensive test coverage before merging.


🔒 Security Considerations

No security concerns identified. The temporal parsing uses standard Java java.time APIs with proper validation. No user input is passed to reflection or dynamic code execution.


Performance Considerations

1. String Parsing Performance

The regex-heavy parsing in CypherDate.parseLocalDate() could be expensive for high-frequency queries. Consider:

  • Caching compiled patterns as static final fields
  • Fast-path checks before regex matching

2. Memory Allocation

Each temporal operation creates new wrapper objects (CypherDate, CypherDuration, etc.). For arithmetic-heavy queries, this could cause GC pressure. Consider:

  • Object pooling for common values (e.g., zero duration)
  • Direct LocalDate/LocalDateTime storage in documents

📝 Recommendations Summary

Must Fix Before Merge 🚨

  1. Add comprehensive unit tests for temporal functionality
  2. Fix division by zero inconsistency
  3. Fix potential overflow in CypherDuration.compareTo()

Should Fix ⚠️

  1. Add temporal subtraction support (date - dateduration)
  2. Add null checks to all temporal constructor functions
  3. Document month-to-days approximation limitations

Nice to Have 💡

  1. Verify negative duration formatting with tests
  2. Optimize string parsing performance
  3. Consider memory allocation impact

📊 Test Results Expected

Per CLAUDE.md: "Run all the connected tests could be affected by the change"

Suggested test commands:

mvn test -Dtest="*OpenCypher*"
mvn test -Dtest="*Cypher*Expression*"
mvn test -Dtest="*Cypher*Function*"

Overall Assessment

Score: 8/10 - Excellent implementation with strong architecture and comprehensive feature coverage. The main concerns are around edge case handling, overflow safety, and missing unit tests. Once these are addressed, this will be a solid addition to ArcadeDB's OpenCypher support.

Verdict: Approve with changes - The implementation is fundamentally sound but needs the critical issues fixed and comprehensive testing added before merge.

Great work on improving OpenCypher compliance! 🎉

Copy link
Contributor

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

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 comprehensive support for temporal data types (Date, Time, LocalDateTime, DateTime, Duration) in OpenCypher expressions. Key changes include adding new CypherTemporalValue wrapper classes for java.time types, implementing temporal arithmetic in ArithmeticExpression, and enabling temporal comparisons in ComparisonExpression. Property access for temporal values (e.g., date.year) is now supported, and numerous new temporal functions (constructors, truncation, epoch, duration calculations) have been added to CypherFunctionFactory. The parsing logic in CypherExpressionBuilder was refactored to correctly handle operator precedence, and list operations were extended to include concatenation. Additionally, CreateStep now converts CypherTemporalValue objects to their underlying java.time types for storage, and toString(), toInteger(), toFloat(), toBoolean() functions were updated for better type handling and error reporting. Review comments highlighted the need to improve robustness in generic CypherTemporalValue conversion, address inefficient object creation in list comparisons, and correct the compareTo method for CypherDuration to align with OpenCypher specifications regarding ordering, as well as refactor duplicated temporal arithmetic logic.

Comment on lines +1118 to +1126
if (args[0] instanceof CypherTemporalValue) {
// Generic temporal → extract date part
final CypherTemporalValue tv = (CypherTemporalValue) args[0];
final Object year = tv.getTemporalProperty("year");
if (year != null)
return new CypherDate(LocalDate.of(((Number) year).intValue(),
((Number) tv.getTemporalProperty("month")).intValue(),
((Number) tv.getTemporalProperty("day")).intValue()));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The fallback logic for converting a generic CypherTemporalValue to a CypherDate is not robust. It checks for the existence of the year property and then assumes that month and day also exist and are of type Number. This could lead to a NullPointerException or ClassCastException at runtime if a temporal value has a year but not the other components. It's safer to check for the existence and type of all required properties before using them.

      if (args[0] instanceof CypherTemporalValue) {
        // Generic temporal → extract date part
        final CypherTemporalValue tv = (CypherTemporalValue) args[0];
        final Object year = tv.getTemporalProperty("year");
        final Object month = tv.getTemporalProperty("month");
        final Object day = tv.getTemporalProperty("day");
        if (year instanceof Number && month instanceof Number && day instanceof Number) {
          return new CypherDate(LocalDate.of(((Number) year).intValue(),
              ((Number) month).intValue(),
              ((Number) day).intValue()));
        }
      }

Comment on lines +180 to +187
public int compareTo(final CypherTemporalValue other) {
if (other instanceof CypherDuration d) {
// Approximate comparison: convert everything to nanoseconds
final long thisTotal = (months * 30 * 86400 + days * 86400 + seconds) * 1_000_000_000L + nanosAdjustment;
final long otherTotal = (d.months * 30 * 86400 + d.days * 86400 + d.seconds) * 1_000_000_000L + d.nanosAdjustment;
return Long.compare(thisTotal, otherTotal);
}
throw new IllegalArgumentException("Cannot compare Duration with " + other.getClass().getSimpleName());
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The compareTo method provides an approximate ordering for durations by converting months to an average of 30 days. However, the openCypher specification states that durations can only be compared for equality, not for ordering (<, >, etc.). This implementation could lead to incorrect or unexpected results for ordering comparisons.

To align with the specification, this method should throw an IllegalArgumentException. ComparisonExpression should then be updated to handle CypherDuration equality checks separately using the .equals() method, and return null for ordering comparisons.

@Override
public int compareTo(final CypherTemporalValue other) {
  if (other instanceof CypherDuration) {
    // Per openCypher spec, Durations can only be compared for equality, not ordering.
    throw new IllegalArgumentException("Durations cannot be ordered.");
  }
  throw new IllegalArgumentException("Cannot compare Duration with " + other.getClass().getSimpleName());
}

Comment on lines +190 to +245
if (leftValue instanceof LocalDate ld && rightValue instanceof CypherDuration dur) {
final LocalDate d = ld.plusMonths(operator == Operator.ADD ? dur.getMonths() : -dur.getMonths())
.plusDays(operator == Operator.ADD ? dur.getDays() : -dur.getDays());
return new CypherDate(d);
}

// java.time.LocalDateTime (from ArcadeDB storage) ± Duration
if (leftValue instanceof LocalDateTime ldt && rightValue instanceof CypherDuration dur) {
LocalDateTime dt = ldt
.plusMonths(operator == Operator.ADD ? dur.getMonths() : -dur.getMonths())
.plusDays(operator == Operator.ADD ? dur.getDays() : -dur.getDays())
.plusSeconds(operator == Operator.ADD ? dur.getSeconds() : -dur.getSeconds())
.plusNanos(operator == Operator.ADD ? dur.getNanosAdjustment() : -dur.getNanosAdjustment());
return new CypherLocalDateTime(dt);
}

// Date ± Duration
if (leftValue instanceof CypherDate cd && rightValue instanceof CypherDuration dur) {
final LocalDate d = cd.getValue().plusMonths(operator == Operator.ADD ? dur.getMonths() : -dur.getMonths())
.plusDays(operator == Operator.ADD ? dur.getDays() : -dur.getDays());
return new CypherDate(d);
}

// LocalTime ± Duration
if (leftValue instanceof CypherLocalTime clt && rightValue instanceof CypherDuration dur) {
final long totalNanos = dur.getSeconds() * 1_000_000_000L + dur.getNanosAdjustment();
final LocalTime t = operator == Operator.ADD ? clt.getValue().plusNanos(totalNanos) : clt.getValue().minusNanos(totalNanos);
return new CypherLocalTime(t);
}

// Time ± Duration
if (leftValue instanceof CypherTime ct && rightValue instanceof CypherDuration dur) {
final long totalNanos = dur.getSeconds() * 1_000_000_000L + dur.getNanosAdjustment();
final OffsetTime t = operator == Operator.ADD ? ct.getValue().plusNanos(totalNanos) : ct.getValue().minusNanos(totalNanos);
return new CypherTime(t);
}

// LocalDateTime ± Duration
if (leftValue instanceof CypherLocalDateTime cldt && rightValue instanceof CypherDuration dur) {
LocalDateTime dt = cldt.getValue()
.plusMonths(operator == Operator.ADD ? dur.getMonths() : -dur.getMonths())
.plusDays(operator == Operator.ADD ? dur.getDays() : -dur.getDays())
.plusSeconds(operator == Operator.ADD ? dur.getSeconds() : -dur.getSeconds())
.plusNanos(operator == Operator.ADD ? dur.getNanosAdjustment() : -dur.getNanosAdjustment());
return new CypherLocalDateTime(dt);
}

// DateTime ± Duration
if (leftValue instanceof CypherDateTime cdt && rightValue instanceof CypherDuration dur) {
ZonedDateTime dt = cdt.getValue()
.plusMonths(operator == Operator.ADD ? dur.getMonths() : -dur.getMonths())
.plusDays(operator == Operator.ADD ? dur.getDays() : -dur.getDays())
.plusSeconds(operator == Operator.ADD ? dur.getSeconds() : -dur.getSeconds())
.plusNanos(operator == Operator.ADD ? dur.getNanosAdjustment() : -dur.getNanosAdjustment());
return new CypherDateTime(dt);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

There is significant code duplication in evaluateTemporalArithmetic for handling java.time types and their corresponding Cypher* wrapper types. For example, the logic for LocalDate and CypherDate is nearly identical, as is the logic for LocalDateTime and CypherLocalDateTime, and ZonedDateTime and CypherDateTime. This can be refactored by extracting helper methods to reduce redundancy and improve maintainability.

Comment on lines +169 to +177
for (int i = 0; i < leftList.size(); i++) {
final Object elemResult = new ComparisonExpression(
new LiteralExpression(leftList.get(i), ""), Operator.EQUALS,
new LiteralExpression(rightList.get(i), ""))
.evaluateTernary(null, null);
if (elemResult == null)
hasNull = true;
else if (!Boolean.TRUE.equals(elemResult))
return operator == Operator.NOT_EQUALS;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Creating new ComparisonExpression and LiteralExpression objects within a loop for each element comparison is inefficient. This can lead to performance issues when comparing large lists due to excessive object allocation. Consider refactoring this to avoid object creation in the loop, for example by extracting the comparison logic into a static helper method that can be called directly with the element values.

@codacy-production
Copy link

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
-13.05% 13.36%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (960fa7f) 78083 54713 70.07%
Head commit (b8b3b46) 108968 (+30885) 62129 (+7416) 57.02% (-13.05%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#3363) 1385 185 13.36%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

robfrank pushed a commit that referenced this pull request Feb 17, 2026
* test: including official opencypher tck

* fix: fixed many issues with opencypher from TCK tests

* fix: fixed broken tests from OpenCyphr TCK

* fix: more opencypher issues from tck tests

* fix: opencypher tck more tests pass now

Reached much better compliance with the latest changes:

```
=======================================================
         OpenCypher TCK Compliance Report
=======================================================
Total scenarios:  3897
Passed:           1908 (48%)
Failed:           1939 (49%)
Skipped:          50 (1%)
-------------------------------------------------------
By category:
  clauses/call                               2/ 52 passed (3%)
  clauses/create                            64/ 78 passed (82%)
  clauses/delete                            24/ 41 passed (58%)
  clauses/match                            292/381 passed (76%)
  clauses/match-where                       25/ 34 passed (73%)
  clauses/merge                             47/ 75 passed (62%)
  clauses/remove                            29/ 33 passed (87%)
  clauses/return                            35/ 63 passed (55%)
  clauses/return-orderby                    23/ 35 passed (65%)
  clauses/return-skip-limit                 26/ 31 passed (83%)
  clauses/set                               30/ 53 passed (56%)
  clauses/union                              8/ 12 passed (66%)
  clauses/unwind                            10/ 14 passed (71%)
  clauses/with                              14/ 29 passed (48%)
  clauses/with-orderBy                     124/292 passed (42%)
  clauses/with-skip-limit                    7/  9 passed (77%)
  clauses/with-where                        10/ 19 passed (52%)
  expressions/aggregation                   23/ 35 passed (65%)
  expressions/boolean                      150/150 passed (100%)
  expressions/comparison                    36/ 72 passed (50%)
  expressions/conditional                   13/ 13 passed (100%)
  expressions/existentialSubqueries          4/ 10 passed (40%)
  expressions/graph                         32/ 61 passed (52%)
  expressions/list                         120/185 passed (64%)
  expressions/literals                     120/131 passed (91%)
  expressions/map                           28/ 44 passed (63%)
  expressions/mathematical                   3/  6 passed (50%)
  expressions/null                          44/ 44 passed (100%)
  expressions/path                           0/  7 passed (0%)
  expressions/pattern                       19/ 50 passed (38%)
  expressions/precedence                    20/121 passed (16%)
  expressions/quantifier                   478/604 passed (79%)
  expressions/string                        22/ 32 passed (68%)
  expressions/temporal                       0/1004 passed (0%)
  expressions/typeConversion                19/ 47 passed (40%)
  useCases/countingSubgraphMatches           6/ 11 passed (54%)
  useCases/triadicSelection                  1/ 19 passed (5%)
=======================================================
```

* fix: opencypher implemented missing temporal functions + precedence

Issue #3357 and #3355

* fix: opencypher implemented more missing functions

Issue #3357 and #3355

(cherry picked from commit 918725f)
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