Visitar URL original
Cypher durations differ from Neo4j: division and multiplication rounding, duration.between and inDays across DST, sum() and avg() refuse durations · Issue #10198 · ArcadeData/arcadedb · GitHub
Skip to content

Cypher durations differ from Neo4j: division and multiplication rounding, duration.between and inDays across DST, sum() and avg() refuse durations #10198

Closed

Description

@tae898

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

  1. added this to the 26.11.1 milestone on Oct 17, 2026
  2. added
    in progressThe team is actively working on this issue
    on Oct 17, 2026
  3. self-assigned this
    on Oct 17, 2026
  4. added 5 commits that reference this issue on Oct 17, 2026
  5. lvca commented on Oct 17, 2026

    @lvca
    Member

    Fixed in #10222 (release 26.11.1).

    • Multiply and divide: * and / on durations are now exact down to the nanosecond, so duration('P1D') / 24 is PT1H. An INTEGER operand is never rounded, so duration('PT0.000000001S') * 9223372036854775807 is exact.
    • Across a DST change: duration.between, inDays and inMonths count calendar days and months in the time zone of the first argument. Noon to noon across a DST change is now P1D, as in Neo4j.
    • sum() and avg(): both accept durations, in SQL too for sum(). 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') / 100 is exactly PT7H18M17.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.

  6. removed
    in progressThe team is actively working on this issue
    on Oct 17, 2026
  7. tae898 commented on Oct 17, 2026

    @tae898
    ContributorAuthor

    Verified on the merge commit b28fa820fc against main a759246daa (where it was filed), with the statements from the issue, each run with db.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.

    statement a759246daa b28fa820fc Neo4j
    RETURN toString(duration('P1D') / 24) AS v PT59M59.999999999S PT1H PT1H
    RETURN toString(duration('PT1H') * 0.3333333333333333) AS v PT19M59.999999999S PT19M59.999999999S PT20M
    RETURN toString(duration('PT0.000000001S') * 9223372036854775807) AS v PT2562047H47M16.854776S PT2562047H47M16.854775807S PT2562047H47M16.854775807S
    RETURN toString(duration('P1M') / 100) AS v PT7H18M17.46S PT7H18M17.46S PT7H18M17.459999999S
    RETURN toString(duration('P1M') * 0.1) AS v P3DT1H2M54.6S P3DT1H2M54.6S P3DT1H2M54.599999999S
    RETURN toString(duration('P1D') / 2) AS v PT12H PT12H PT12H
    RETURN toString(duration.between(datetime('2024-03-30T12:00[Europe/Paris]'), datetime('2024-03-31T12:00[Europe/Paris]'))) AS v PT23H P1D P1D
    RETURN toString(duration.between(datetime('2024-10-26T12:00[Europe/Paris]'), datetime('2024-10-27T12:00[Europe/Paris]'))) AS v P1DT1H P1D P1D
    RETURN toString(duration.between(datetime('2024-01-01T00:00[Europe/Paris]'), datetime('2024-07-01T00:00[Europe/Paris]'))) AS v P5M29DT23H P6M P6M
    RETURN toString(duration.between(datetime('2024-01-01T00:00+01:00'), datetime('2024-07-01T00:00+02:00'))) AS v P5M29DT23H P5M29DT23H P5M29DT23H
    RETURN toString(duration.between(datetime('2024-01-15T12:00[Australia/Lord_Howe]'), datetime('2024-06-15T12:00[UTC]'))) AS v P5MT11H P5MT10H30M P5MT10H30M
    RETURN toString(duration.between(date('2024-01-31'), datetime('2024-10-27T01:59[Europe/Paris]'))) AS v P8M26DT59M P8M27DT1H59M P8M27DT1H59M
    RETURN toString(duration.between(datetime('2024-01-01T00:00Z'), datetime('2024-07-01T00:00[Europe/Paris]'))) AS v P5M29DT22H P5M29DT22H P5M29DT22H
    RETURN toString(duration.between(datetime('2024-01-01T00:00Z'), datetime('2024-07-01T00:00Z'))) AS v P6M P6M P6M
    RETURN toString(duration.inDays(datetime('2024-03-30T12:00[Europe/Paris]'), datetime('2024-03-31T12:00[Europe/Paris]'))) AS v PT0S P1D 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 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 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 typ P1M1D 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 argume P15DT17H14M33S P15DT17H14M33S

    19 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.

  8. added a commit that references this issue on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugopencypherseverity:minorCosmetic, documentation, edge case, or a workaround exists

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions