Repository navigation
Description
Cypher duration arithmetic differs from Neo4j in three ways: dividing or multiplying a duration by a number rounds differently, and in the plainest case wrongly (duration('P1D') / 24 is PT59M59.999999999S, not PT1H); duration.between and duration.inDays across a daylight-saving change answer in absolute hours (PT23H) where Neo4j counts the calendar day (P1D); and sum() and avg() refuse durations, which Cypher aggregates.
Identical on main (a759246daa) and 26.10.2 (e5040d0339), JDK 25 (-XX:+UseCompactObjectHeaders) and JDK 21, each statement run with db.command("opencypher", ...); Neo4j 2026.08.1 as the reference. Found by a temporal differential against Neo4j (8,668 generated queries; rows marked control agree). ArcadeDB's toString() also drops zero seconds (12:30 against Neo4j's 12:30:00); where that is the only difference in a row, it is formatting, not the point of this issue.
| statement | ArcadeDB | Neo4j |
|---|---|---|
RETURN toString(duration('P1D') / 24) AS v |
PT59M59.999999999S | PT1H |
RETURN toString(duration('PT1H') * 0.3333333333333333) AS v |
PT19M59.999999999S | PT20M |
RETURN toString(duration('PT0.000000001S') * 9223372036854775807) AS v |
PT2562047H47M16.854776S | PT2562047H47M16.854775807S |
RETURN toString(duration('P1M') / 100) AS v |
PT7H18M17.46S | PT7H18M17.459999999S |
RETURN toString(duration('P1M') * 0.1) AS v |
P3DT1H2M54.6S | P3DT1H2M54.599999999S |
RETURN toString(duration('P1D') / 2) AS v |
PT12H | PT12H |
RETURN toString(duration.between(datetime('2024-03-30T12:00[Europe/Paris]'), datetime('2024-03-31T12:00[Europe/Paris]'))) AS v |
PT23H | P1D |
RETURN toString(duration.between(datetime('2024-10-26T12:00[Europe/Paris]'), datetime('2024-10-27T12:00[Europe/Paris]'))) AS v |
P1DT1H | P1D |
RETURN toString(duration.between(datetime('2024-01-01T00:00[Europe/Paris]'), datetime('2024-07-01T00:00[Europe/Paris]'))) AS v |
P5M29DT23H | P6M |
RETURN toString(duration.between(datetime('2024-01-01T00:00+01:00'), datetime('2024-07-01T00:00+02:00'))) AS v |
P5M29DT23H | P5M29DT23H |
RETURN toString(duration.between(datetime('2024-01-15T12:00[Australia/Lord_Howe]'), datetime('2024-06-15T12:00[UTC]'))) AS v |
P5MT11H | P5MT10H30M |
RETURN toString(duration.between(date('2024-01-31'), datetime('2024-10-27T01:59[Europe/Paris]'))) AS v |
P8M26DT59M | P8M27DT1H59M |
RETURN toString(duration.between(datetime('2024-01-01T00:00Z'), datetime('2024-07-01T00:00[Europe/Paris]'))) AS v |
P5M29DT22H | P5M29DT22H |
RETURN toString(duration.between(datetime('2024-01-01T00:00Z'), datetime('2024-07-01T00:00Z'))) AS v |
P6M | P6M |
RETURN toString(duration.inDays(datetime('2024-03-30T12:00[Europe/Paris]'), datetime('2024-03-31T12:00[Europe/Paris]'))) AS v |
PT0S | P1D |
RETURN toString(duration.inDays(datetime('2024-10-27T01:59[Europe/Paris]'), datetime('2024-02-29T12:30-12:00'))) AS v |
P-239D | P-240D |
RETURN toString(duration.inDays(datetime('1945-06-01T12:00[Europe/Berlin]'), datetime('9999-12-31T23:59:59.999999999+14:00'))) AS v |
P2941876D | P2941875D |
UNWIND [duration('P1M'), duration('P1D')] AS x RETURN toString(sum(x)) AS v |
error: IllegalArgumentException: sum() requires numeric input, but received a value of type CypherDuration | P1M1D |
UNWIND [duration('P1M'), duration('P1D')] AS x RETURN toString(avg(x)) AS v |
error: CommandSemanticException: Type mismatch: avg() expects an INTEGER or a FLOAT argument but got DURATION | P15DT17H14M33S |
The program that ran the ArcadeDB side is a loop of db.command("opencypher", statement) over the statements above, one fresh database, each statement in a transaction that is rolled back.
Activity
review(#10198, #10200): named Cypher type-rank constants, release not…
review(#10198, #10200): pin the Cypher order of types, explain the du…
review(#10198, #10199, #10200): resolve date field overrides together…
review(#10198, #10200): pin mixed number/duration on the unboxed sum …
fix(#10198, #10199, #10200, #10203): Cypher temporal semantics aligne…
lvca commentedon Oct 17, 2026
Fixed in #10222 (release 26.11.1).
*and/on durations are now exact down to the nanosecond, soduration('P1D') / 24isPT1H. An INTEGER operand is never rounded, soduration('PT0.000000001S') * 9223372036854775807is exact.duration.between,inDaysandinMonthscount calendar days and months in the time zone of the first argument. Noon to noon across a DST change is nowP1D, as in Neo4j.sum()andavg(): both accept durations, in SQL too forsum(). Mixing numbers and durations is an error.One small, deliberate difference from Neo4j: a FLOAT operand is used as the decimal it is written as, so
duration('P1M') / 100is exactlyPT7H18M17.46S. Neo4j computes in double precision and returns...17.459999999S.❤️ Thanks for helping make ArcadeDB better! ArcadeDB is free and open source, sustained by its community. If it's useful to you or your company, please consider becoming a sponsor to keep development going.
tae898 commentedon Oct 17, 2026
Verified on the merge commit
b28fa820fcagainst maina759246daa(where it was filed), with the statements from the issue, each run withdb.command("opencypher", ...)in a fresh database, on eclipse-temurin 25 (-XX:+UseCompactObjectHeaders) and 21 (identical on both JDKs, before and after); Neo4j 2026.08.1 as the reference.a759246daab28fa820fcRETURN toString(duration('P1D') / 24) AS vRETURN toString(duration('PT1H') * 0.3333333333333333) AS vRETURN toString(duration('PT0.000000001S') * 9223372036854775807) AS vRETURN toString(duration('P1M') / 100) AS vRETURN toString(duration('P1M') * 0.1) AS vRETURN toString(duration('P1D') / 2) AS vRETURN toString(duration.between(datetime('2024-03-30T12:00[Europe/Paris]'), datetime('2024-03-31T12:00[Europe/Paris]'))) AS vRETURN toString(duration.between(datetime('2024-10-26T12:00[Europe/Paris]'), datetime('2024-10-27T12:00[Europe/Paris]'))) AS vRETURN toString(duration.between(datetime('2024-01-01T00:00[Europe/Paris]'), datetime('2024-07-01T00:00[Europe/Paris]'))) AS vRETURN toString(duration.between(datetime('2024-01-01T00:00+01:00'), datetime('2024-07-01T00:00+02:00'))) AS vRETURN toString(duration.between(datetime('2024-01-15T12:00[Australia/Lord_Howe]'), datetime('2024-06-15T12:00[UTC]'))) AS vRETURN toString(duration.between(date('2024-01-31'), datetime('2024-10-27T01:59[Europe/Paris]'))) AS vRETURN toString(duration.between(datetime('2024-01-01T00:00Z'), datetime('2024-07-01T00:00[Europe/Paris]'))) AS vRETURN toString(duration.between(datetime('2024-01-01T00:00Z'), datetime('2024-07-01T00:00Z'))) AS vRETURN toString(duration.inDays(datetime('2024-03-30T12:00[Europe/Paris]'), datetime('2024-03-31T12:00[Europe/Paris]'))) AS vRETURN toString(duration.inDays(datetime('2024-10-27T01:59[Europe/Paris]'), datetime('2024-02-29T12:30-12:00'))) AS vRETURN toString(duration.inDays(datetime('1945-06-01T12:00[Europe/Berlin]'), datetime('9999-12-31T23:59:59.999999999+14:00'))) AS vUNWIND [duration('P1M'), duration('P1D')] AS x RETURN toString(sum(x)) AS vUNWIND [duration('P1M'), duration('P1D')] AS x RETURN toString(avg(x)) AS v19 statements: 16 now answer as Neo4j does; 3 still differ, the FLOAT-operand cases the PR describes as a deliberate difference (the result is the exact decimal, Neo4j's carries floating-point noise). Thanks for the quick fix.
fix(#10198, #10199, #10200, #10203): Cypher temporal semantics aligne…