Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ private Object compareValuesTernary(final Object left, final Object right) {
case GREATER_THAN_OR_EQUAL -> cmp >= 0;
};
} catch (final IllegalArgumentException e) {
return null; // Incompatible temporal types
// Different temporal types: for equality, return false/true; for ordering, return null
if (operator == Operator.EQUALS) return false;
if (operator == Operator.NOT_EQUALS) return true;
return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public PropertyAccessExpression(final String variableName, final String property
public Object evaluate(final Result result, final CommandContext context) {
final Object variable = result.getProperty(variableName);
if (variable instanceof Document) {
return convertFromStorage(((Document) variable).get(propertyName));
final Object rawValue = ((Document) variable).get(propertyName);
return convertFromStorage(rawValue);
} else if (variable instanceof Map) {
// Handle Map types (e.g., from UNWIND with parameter maps)
return ((Map<?, ?>) variable).get(propertyName);
Expand Down Expand Up @@ -88,6 +89,23 @@ public String getPropertyName() {
* doesn't have native binary types for them.
*/
private static Object convertFromStorage(final Object value) {
// Handle collections (lists/arrays of temporal values)
if (value instanceof java.util.Collection<?> collection) {
final java.util.List<Object> converted = new java.util.ArrayList<>(collection.size());
for (final Object item : collection) {
converted.add(convertFromStorage(item));
}
return converted;
}
if (value instanceof Object[] array) {
final Object[] converted = new Object[array.length];
for (int i = 0; i < array.length; i++) {
converted[i] = convertFromStorage(array[i]);
}
return converted;
}

// Handle single values
if (value instanceof LocalDate ld)
return new CypherDate(ld);
if (value instanceof LocalDateTime ldt)
Expand All @@ -101,6 +119,18 @@ private static Object convertFromStorage(final Object value) {
// Not a valid duration string
}
}

// DateTime strings: contain 'T' with date part before it and timezone/offset
// e.g., 1912-01-01T00:00Z, 1984-10-11T12:31:14+01:00[Europe/Stockholm]
final int tIdx = str.indexOf('T');
if (tIdx >= 4 && tIdx < str.length() - 1 && Character.isDigit(str.charAt(0))) {
try {
return CypherDateTime.parse(str);
} catch (final Exception ignored) {
// Not a valid datetime string
}
}

// Time strings: HH:MM:SS[.nanos][+/-offset] or HH:MM:SS[.nanos]Z
if (str.length() >= 8 && str.charAt(2) == ':' && str.charAt(5) == ':') {
// Check if it has a timezone offset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
Expand Down Expand Up @@ -1139,12 +1140,35 @@

// ======================== Temporal Functions ========================

/**
* Get or initialize statement time for temporal constructors.
* In Cypher, temporal functions like date(), localtime(), etc. should return the same
* frozen time throughout the entire query execution to ensure consistent results.
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> getStatementTime(final CommandContext context) {
Map<String, Object> statementTime = (Map<String, Object>) context.getVariable("$statementTime");
if (statementTime == null) {
// First call - freeze the current time
statementTime = new java.util.HashMap<>();

Check notice on line 1153 in engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherFunctionFactory.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherFunctionFactory.java#L1153

Unnecessary use of fully qualified name 'java.util.HashMap' due to existing import 'java.util.*'
statementTime.put("date", CypherDate.now());
statementTime.put("localtime", CypherLocalTime.now());
statementTime.put("time", CypherTime.now());
statementTime.put("localdatetime", CypherLocalDateTime.now());
statementTime.put("datetime", CypherDateTime.now());
context.setVariable("$statementTime", statementTime);
}
return statementTime;
}

@SuppressWarnings("unchecked")
private static class DateConstructorFunction implements StatelessFunction {
@Override public String getName() { return "date"; }
@Override public Object execute(final Object[] args, final CommandContext context) {
if (args.length == 0)
return CypherDate.now();
return getStatementTime(context).get("date");
if (args[0] == null)
return null;
if (args[0] instanceof String)
return CypherDate.parse((String) args[0]);
if (args[0] instanceof Map)
Expand Down Expand Up @@ -1177,7 +1201,9 @@
@Override public String getName() { return "localtime"; }
@Override public Object execute(final Object[] args, final CommandContext context) {
if (args.length == 0)
return CypherLocalTime.now();
return getStatementTime(context).get("localtime");
if (args[0] == null)
return null;
if (args[0] instanceof String)
return CypherLocalTime.parse((String) args[0]);
if (args[0] instanceof Map)
Expand All @@ -1201,7 +1227,9 @@
@Override public String getName() { return "time"; }
@Override public Object execute(final Object[] args, final CommandContext context) {
if (args.length == 0)
return CypherTime.now();
return getStatementTime(context).get("time");
if (args[0] == null)
return null;
if (args[0] instanceof String)
return CypherTime.parse((String) args[0]);
if (args[0] instanceof Map)
Expand All @@ -1223,7 +1251,9 @@
@Override public String getName() { return "localdatetime"; }
@Override public Object execute(final Object[] args, final CommandContext context) {
if (args.length == 0)
return CypherLocalDateTime.now();
return getStatementTime(context).get("localdatetime");
if (args[0] == null)
return null;
if (args[0] instanceof String)
return CypherLocalDateTime.parse((String) args[0]);
if (args[0] instanceof Map)
Expand All @@ -1249,7 +1279,9 @@
@Override public String getName() { return "datetime"; }
@Override public Object execute(final Object[] args, final CommandContext context) {
if (args.length == 0)
return CypherDateTime.now();
return getStatementTime(context).get("datetime");
if (args[0] == null)
return null;
if (args[0] instanceof String)
return CypherDateTime.parse((String) args[0]);
if (args[0] instanceof Map)
Expand All @@ -1274,6 +1306,8 @@
@Override public Object execute(final Object[] args, final CommandContext context) {
if (args.length != 1)
throw new CommandExecutionException("duration() requires exactly one argument");
if (args[0] == null)
return null;
if (args[0] instanceof String)
return CypherDuration.parse((String) args[0]);
if (args[0] instanceof Map)
Expand Down Expand Up @@ -1359,9 +1393,14 @@
else
throw new CommandExecutionException("time.truncate() second argument must be a temporal value with a time");
LocalTime truncated = TemporalUtil.truncateLocalTime(time.toLocalTime(), unit);
if (args.length >= 3 && args[2] instanceof Map)
truncated = applyTimeMap(truncated, (Map<String, Object>) args[2]);
return new CypherTime(OffsetTime.of(truncated, time.getOffset()));
ZoneOffset offset = time.getOffset();
if (args.length >= 3 && args[2] instanceof Map) {
final Map<String, Object> adjustMap = (Map<String, Object>) args[2];
truncated = applyTimeMap(truncated, adjustMap);
if (adjustMap.containsKey("timezone"))
offset = TemporalUtil.parseOffset(adjustMap.get("timezone").toString());
}
return new CypherTime(OffsetTime.of(truncated, offset));
}
}

Expand Down Expand Up @@ -1413,9 +1452,14 @@
else
throw new CommandExecutionException("datetime.truncate() second argument must be a temporal value");
LocalDateTime truncated = TemporalUtil.truncateLocalDateTime(dt.toLocalDateTime(), unit);
if (args.length >= 3 && args[2] instanceof Map)
truncated = applyDateTimeMap(truncated, (Map<String, Object>) args[2]);
return new CypherDateTime(ZonedDateTime.of(truncated, dt.getZone()));
ZoneId zone = dt.getZone();
if (args.length >= 3 && args[2] instanceof Map) {
final Map<String, Object> adjustMap = (Map<String, Object>) args[2];
truncated = applyDateTimeMap(truncated, adjustMap);
if (adjustMap.containsKey("timezone"))
zone = TemporalUtil.parseZone(adjustMap.get("timezone").toString());
}
return new CypherDateTime(ZonedDateTime.of(truncated, zone));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,14 +314,32 @@

/**
* Convert CypherTemporalValue objects to java.time types for ArcadeDB storage.
* Handles both single values and collections/arrays of temporal values.
*/
private static Object convertTemporalForStorage(final Object value) {
// Handle collections (lists/arrays of temporal values)
if (value instanceof java.util.Collection<?> collection) {
final java.util.List<Object> converted = new java.util.ArrayList<>(collection.size());

Check notice on line 322 in engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/CreateStep.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/CreateStep.java#L322

Unnecessary use of fully qualified name 'java.util.ArrayList' due to existing import 'java.util.ArrayList'
for (final Object item : collection) {
converted.add(convertTemporalForStorage(item));
}
return converted;
}
if (value instanceof Object[] array) {
final Object[] converted = new Object[array.length];
for (int i = 0; i < array.length; i++) {
converted[i] = convertTemporalForStorage(array[i]);
}
return converted;
}

// Handle single temporal values
if (value instanceof CypherDate)
return ((CypherDate) value).getValue();
if (value instanceof CypherLocalDateTime)
return ((CypherLocalDateTime) value).getValue();
if (value instanceof CypherDateTime)
return ((CypherDateTime) value).getValue().toLocalDateTime();
return value.toString(); // Store as String to preserve timezone info
if (value instanceof CypherLocalTime)
return ((CypherLocalTime) value).getValue().toString();
if (value instanceof CypherTime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import com.arcadedb.query.sql.executor.Result;
import com.arcadedb.query.sql.executor.ResultSet;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
Expand Down Expand Up @@ -134,12 +136,79 @@
return null;

if (obj instanceof Vertex)
return ((Vertex) obj).get(parts[1]);
return convertFromStorage(((Vertex) obj).get(parts[1]));
else if (obj instanceof Edge)
return ((Edge) obj).get(parts[1]);
return convertFromStorage(((Edge) obj).get(parts[1]));
}

return result.getProperty(expression);
return convertFromStorage(result.getProperty(expression));
}

/**
* Convert ArcadeDB-stored values back to Cypher temporal types for proper comparison.
* Duration, LocalTime, and Time are stored as Strings because ArcadeDB
* doesn't have native binary types for them.
*/
private static Object convertFromStorage(final Object value) {
// Handle collections (lists/arrays of temporal values)
if (value instanceof java.util.Collection<?> collection) {
final java.util.List<Object> converted = new java.util.ArrayList<>(collection.size());

Check notice on line 155 in engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/OrderByStep.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/OrderByStep.java#L155

Unnecessary use of fully qualified name 'java.util.ArrayList' due to existing import 'java.util.ArrayList'
for (final Object item : collection) {
converted.add(convertFromStorage(item));
}
return converted;
}
if (value instanceof Object[] array) {
final Object[] converted = new Object[array.length];
for (int i = 0; i < array.length; i++) {
converted[i] = convertFromStorage(array[i]);
}
return converted;
}

// Handle single values
if (value instanceof LocalDate ld)
return new CypherDate(ld);
if (value instanceof LocalDateTime ldt)
return new CypherLocalDateTime(ldt);
if (value instanceof String str) {
// Duration strings start with P (ISO-8601)
if (str.length() > 1 && str.charAt(0) == 'P') {
try {
return CypherDuration.parse(str);
} catch (final Exception ignored) {
// Not a valid duration string
}
}
// DateTime strings: contain 'T' with date part before it and timezone/offset
final int tIdx = str.indexOf('T');
if (tIdx >= 4 && tIdx < str.length() - 1 && Character.isDigit(str.charAt(0))) {
try {
return CypherDateTime.parse(str);
} catch (final Exception ignored) {
// Not a valid datetime string
}
}
// Time strings: HH:MM:SS[.nanos][+/-offset] or HH:MM:SS[.nanos]Z
if (str.length() >= 8 && str.charAt(2) == ':' && str.charAt(5) == ':') {
// Check if it has a timezone offset
final boolean hasOffset = str.contains("+") || str.contains("-") || str.endsWith("Z");
if (hasOffset) {
try {
return CypherTime.parse(str);
} catch (final Exception ignored) {
// Not a valid time string
}
} else {
try {
return CypherLocalTime.parse(str);
} catch (final Exception ignored) {
// Not a valid local time string
}
}
}
}
return value;
}
Comment on lines +148 to 212
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The convertFromStorage method is a duplicate of the one found in PropertyAccessExpression. This duplication increases maintenance overhead. Consider refactoring this method into a shared utility class, such as TemporalUtil, so it can be reused by both OrderByStep and PropertyAccessExpression.


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1076,8 +1076,11 @@ public Map<String, Object> visitMap(final Cypher25Parser.MapContext ctx) {
final ParameterExpression paramExpr = (ParameterExpression) expr;
value = new ParameterReference(paramExpr.getParameterName());
} else if (expr instanceof ListExpression) {
// Evaluate list literals immediately
value = expr.evaluate(null, null);
// Evaluate list literals immediately, but only if all elements are simple literals
if (isStaticListExpression((ListExpression) expr))
value = expr.evaluate(null, null);
else
value = expr; // Keep as Expression for runtime evaluation (e.g., [date({...})])
} else {
// Keep dynamic expressions as Expression objects for runtime evaluation
value = expr;
Expand All @@ -1089,6 +1092,14 @@ public Map<String, Object> visitMap(final Cypher25Parser.MapContext ctx) {
return map;
}

private static boolean isStaticListExpression(final ListExpression listExpr) {
for (final Expression element : listExpr.getElements()) {
if (!(element instanceof LiteralExpression))
return false;
}
return true;
}

private List<String> extractLabels(final Cypher25Parser.LabelExpressionContext ctx) {
// Delegate to ParserUtils for grammar-based label extraction
return ParserUtils.extractLabels(ctx);
Expand Down
Loading
Loading