From 30e809b9474f442d65f52c4ea92a8b17137a4fd3 Mon Sep 17 00:00:00 2001 From: robfrank Date: Wed, 29 Apr 2026 12:42:36 +0200 Subject: [PATCH 1/2] #4018 fix: Cypher -> temporal component access on date/datetime function calls returns null ChainedPropertyAccessExpression (used when the base is a function call like date().year) was missing temporal type handling, falling through to null. Added CypherTemporalValue, LocalDate, and LocalDateTime branches mirroring PropertyAccessExpression. Co-Authored-By: Claude Sonnet 4.6 --- .../parser/CypherExpressionBuilder.java | 16 ++++ ...herTemporalFunctionsComprehensiveTest.java | 87 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java b/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java index bd297154f7..da4c4a4591 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java @@ -22,9 +22,14 @@ import com.arcadedb.exception.CommandParsingException; import com.arcadedb.query.opencypher.ast.*; import com.arcadedb.query.opencypher.grammar.Cypher25Parser; +import com.arcadedb.query.opencypher.temporal.CypherDate; +import com.arcadedb.query.opencypher.temporal.CypherLocalDateTime; +import com.arcadedb.query.opencypher.temporal.CypherTemporalValue; import com.arcadedb.query.sql.executor.CommandContext; import com.arcadedb.query.sql.executor.Result; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -1662,6 +1667,17 @@ public Object evaluate(final Result result, final CommandContext context) { return ((Result) baseValue).getProperty(propertyName); } + // Handle temporal types (e.g., date().year, datetime().hour) + if (baseValue instanceof CypherTemporalValue) { + return ((CypherTemporalValue) baseValue).getTemporalProperty(propertyName); + } + if (baseValue instanceof LocalDate) { + return new CypherDate((LocalDate) baseValue).getTemporalProperty(propertyName); + } + if (baseValue instanceof LocalDateTime) { + return new CypherLocalDateTime((LocalDateTime) baseValue).getTemporalProperty(propertyName); + } + return null; } diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java index fb850f993f..6e35761f23 100644 --- a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java @@ -1102,4 +1102,91 @@ WITH datetime('2015-07-21T21:40:32.142+0100') AS dt \ final String formatted = (String) result.next().getProperty("result"); assertThat(formatted).isEqualTo("2015-07-21"); } + + // ==================== Temporal component accessor tests (issue #4018) ==================== + + @Test + void dateYearComponentFromLiteral() { + final ResultSet result = database.command("opencypher", "RETURN date('2020-01-15').year AS y"); + Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(((Number) result.next().getProperty("y")).longValue()).isEqualTo(2020L); + } + + @Test + void dateMonthComponentFromLiteral() { + final ResultSet result = database.command("opencypher", "RETURN date('2020-06-30').month AS m"); + Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(((Number) result.next().getProperty("m")).longValue()).isEqualTo(6L); + } + + @Test + void dateDayComponentFromLiteral() { + final ResultSet result = database.command("opencypher", "RETURN date('2020-06-30').day AS d"); + Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(((Number) result.next().getProperty("d")).longValue()).isEqualTo(30L); + } + + @Test + void dateComponentsFromCurrentDate() { + final java.time.LocalDate today = java.time.LocalDate.now(); + final ResultSet result = database.command("opencypher", "RETURN date().year AS y, date().month AS m, date().day AS d"); + Assertions.assertThat(result.hasNext()).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("y")).longValue()).isEqualTo((long) today.getYear()); + assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo((long) today.getMonthValue()); + assertThat(((Number) row.getProperty("d")).longValue()).isEqualTo((long) today.getDayOfMonth()); + } + + @Test + void datetimeYearComponentFromLiteral() { + final ResultSet result = database.command("opencypher", "RETURN datetime('2021-03-14T15:09:26Z').year AS y"); + Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(((Number) result.next().getProperty("y")).longValue()).isEqualTo(2021L); + } + + @Test + void datetimeHourComponentFromLiteral() { + final ResultSet result = database.command("opencypher", "RETURN datetime('2021-03-14T15:09:26Z').hour AS h"); + Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(((Number) result.next().getProperty("h")).longValue()).isEqualTo(15L); + } + + @Test + void datetimeYearFromCurrentDatetime() { + final java.time.ZonedDateTime now = java.time.ZonedDateTime.now(); + final ResultSet result = database.command("opencypher", "RETURN datetime().year AS y"); + Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(((Number) result.next().getProperty("y")).longValue()).isEqualTo((long) now.getYear()); + } + + @Test + void localdatetimeComponentsFromLiteral() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime('2022-11-05T08:30:00').year AS y, localdatetime('2022-11-05T08:30:00').month AS m, localdatetime('2022-11-05T08:30:00').hour AS h"); + Assertions.assertThat(result.hasNext()).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("y")).longValue()).isEqualTo(2022L); + assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo(11L); + assertThat(((Number) row.getProperty("h")).longValue()).isEqualTo(8L); + } + + @Test + void localtimeComponentsFromLiteral() { + final ResultSet result = database.command("opencypher", + "RETURN localtime('10:35:00').hour AS h, localtime('10:35:00').minute AS m"); + Assertions.assertThat(result.hasNext()).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("h")).longValue()).isEqualTo(10L); + assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo(35L); + } + + @Test + void timeComponentsFromLiteral() { + final ResultSet result = database.command("opencypher", + "RETURN time('12:00:00+01:00').hour AS h, time('12:00:00+01:00').minute AS m"); + Assertions.assertThat(result.hasNext()).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("h")).longValue()).isEqualTo(12L); + assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo(0L); + } } From 3e73b4bc2a40bfc5fc302ee4fb8ab5e1d7d4dc4d Mon Sep 17 00:00:00 2001 From: robfrank Date: Wed, 29 Apr 2026 12:47:14 +0200 Subject: [PATCH 2/2] style: add java.time imports and use static assertThat in temporal tests Co-Authored-By: Claude Sonnet 4.6 --- ...herTemporalFunctionsComprehensiveTest.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java index 6e35761f23..d661d07d5e 100644 --- a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java @@ -22,6 +22,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.LocalDate; +import java.time.ZonedDateTime; + import static org.assertj.core.api.Assertions.assertThat; import org.assertj.core.api.Assertions; @@ -1108,29 +1111,29 @@ WITH datetime('2015-07-21T21:40:32.142+0100') AS dt \ @Test void dateYearComponentFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN date('2020-01-15').year AS y"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); assertThat(((Number) result.next().getProperty("y")).longValue()).isEqualTo(2020L); } @Test void dateMonthComponentFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN date('2020-06-30').month AS m"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); assertThat(((Number) result.next().getProperty("m")).longValue()).isEqualTo(6L); } @Test void dateDayComponentFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN date('2020-06-30').day AS d"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); assertThat(((Number) result.next().getProperty("d")).longValue()).isEqualTo(30L); } @Test void dateComponentsFromCurrentDate() { - final java.time.LocalDate today = java.time.LocalDate.now(); + final LocalDate today = LocalDate.now(); final ResultSet result = database.command("opencypher", "RETURN date().year AS y, date().month AS m, date().day AS d"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); final var row = result.next(); assertThat(((Number) row.getProperty("y")).longValue()).isEqualTo((long) today.getYear()); assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo((long) today.getMonthValue()); @@ -1140,22 +1143,22 @@ void dateComponentsFromCurrentDate() { @Test void datetimeYearComponentFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN datetime('2021-03-14T15:09:26Z').year AS y"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); assertThat(((Number) result.next().getProperty("y")).longValue()).isEqualTo(2021L); } @Test void datetimeHourComponentFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN datetime('2021-03-14T15:09:26Z').hour AS h"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); assertThat(((Number) result.next().getProperty("h")).longValue()).isEqualTo(15L); } @Test void datetimeYearFromCurrentDatetime() { - final java.time.ZonedDateTime now = java.time.ZonedDateTime.now(); + final ZonedDateTime now = ZonedDateTime.now(); final ResultSet result = database.command("opencypher", "RETURN datetime().year AS y"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); assertThat(((Number) result.next().getProperty("y")).longValue()).isEqualTo((long) now.getYear()); } @@ -1163,7 +1166,7 @@ void datetimeYearFromCurrentDatetime() { void localdatetimeComponentsFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN localdatetime('2022-11-05T08:30:00').year AS y, localdatetime('2022-11-05T08:30:00').month AS m, localdatetime('2022-11-05T08:30:00').hour AS h"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); final var row = result.next(); assertThat(((Number) row.getProperty("y")).longValue()).isEqualTo(2022L); assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo(11L); @@ -1174,7 +1177,7 @@ void localdatetimeComponentsFromLiteral() { void localtimeComponentsFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN localtime('10:35:00').hour AS h, localtime('10:35:00').minute AS m"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); final var row = result.next(); assertThat(((Number) row.getProperty("h")).longValue()).isEqualTo(10L); assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo(35L); @@ -1184,7 +1187,7 @@ void localtimeComponentsFromLiteral() { void timeComponentsFromLiteral() { final ResultSet result = database.command("opencypher", "RETURN time('12:00:00+01:00').hour AS h, time('12:00:00+01:00').minute AS m"); - Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(result.hasNext()).isTrue(); final var row = result.next(); assertThat(((Number) row.getProperty("h")).longValue()).isEqualTo(12L); assertThat(((Number) row.getProperty("m")).longValue()).isEqualTo(0L);