Skip to content

Commit c8b8ce5

Browse files
authored
Opencypher tck (#3368)
* 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 * fix: opencypher more code fixed thank to the tck * fix: OpenCypher fixed temporal functions (from TCK results)
1 parent b06b4c1 commit c8b8ce5

11 files changed

Lines changed: 372 additions & 44 deletions

File tree

engine/src/main/java/com/arcadedb/query/opencypher/ast/ComparisonExpression.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ private Object compareValuesTernary(final Object left, final Object right) {
112112
case GREATER_THAN_OR_EQUAL -> cmp >= 0;
113113
};
114114
} catch (final IllegalArgumentException e) {
115-
return null; // Incompatible temporal types
115+
// Different temporal types: for equality, return false/true; for ordering, return null
116+
if (operator == Operator.EQUALS) return false;
117+
if (operator == Operator.NOT_EQUALS) return true;
118+
return null;
116119
}
117120
}
118121

engine/src/main/java/com/arcadedb/query/opencypher/ast/PropertyAccessExpression.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public PropertyAccessExpression(final String variableName, final String property
4444
public Object evaluate(final Result result, final CommandContext context) {
4545
final Object variable = result.getProperty(variableName);
4646
if (variable instanceof Document) {
47-
return convertFromStorage(((Document) variable).get(propertyName));
47+
final Object rawValue = ((Document) variable).get(propertyName);
48+
return convertFromStorage(rawValue);
4849
} else if (variable instanceof Map) {
4950
// Handle Map types (e.g., from UNWIND with parameter maps)
5051
return ((Map<?, ?>) variable).get(propertyName);
@@ -88,6 +89,23 @@ public String getPropertyName() {
8889
* doesn't have native binary types for them.
8990
*/
9091
private static Object convertFromStorage(final Object value) {
92+
// Handle collections (lists/arrays of temporal values)
93+
if (value instanceof java.util.Collection<?> collection) {
94+
final java.util.List<Object> converted = new java.util.ArrayList<>(collection.size());
95+
for (final Object item : collection) {
96+
converted.add(convertFromStorage(item));
97+
}
98+
return converted;
99+
}
100+
if (value instanceof Object[] array) {
101+
final Object[] converted = new Object[array.length];
102+
for (int i = 0; i < array.length; i++) {
103+
converted[i] = convertFromStorage(array[i]);
104+
}
105+
return converted;
106+
}
107+
108+
// Handle single values
91109
if (value instanceof LocalDate ld)
92110
return new CypherDate(ld);
93111
if (value instanceof LocalDateTime ldt)
@@ -101,6 +119,18 @@ private static Object convertFromStorage(final Object value) {
101119
// Not a valid duration string
102120
}
103121
}
122+
123+
// DateTime strings: contain 'T' with date part before it and timezone/offset
124+
// e.g., 1912-01-01T00:00Z, 1984-10-11T12:31:14+01:00[Europe/Stockholm]
125+
final int tIdx = str.indexOf('T');
126+
if (tIdx >= 4 && tIdx < str.length() - 1 && Character.isDigit(str.charAt(0))) {
127+
try {
128+
return CypherDateTime.parse(str);
129+
} catch (final Exception ignored) {
130+
// Not a valid datetime string
131+
}
132+
}
133+
104134
// Time strings: HH:MM:SS[.nanos][+/-offset] or HH:MM:SS[.nanos]Z
105135
if (str.length() >= 8 && str.charAt(2) == ':' && str.charAt(5) == ':') {
106136
// Check if it has a timezone offset

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

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.time.LocalDateTime;
3838
import java.time.LocalTime;
3939
import java.time.OffsetTime;
40+
import java.time.ZoneId;
4041
import java.time.ZoneOffset;
4142
import java.time.ZonedDateTime;
4243
import java.util.*;
@@ -1139,12 +1140,35 @@ public int hashCode() {
11391140

11401141
// ======================== Temporal Functions ========================
11411142

1143+
/**
1144+
* Get or initialize statement time for temporal constructors.
1145+
* In Cypher, temporal functions like date(), localtime(), etc. should return the same
1146+
* frozen time throughout the entire query execution to ensure consistent results.
1147+
*/
1148+
@SuppressWarnings("unchecked")
1149+
private static Map<String, Object> getStatementTime(final CommandContext context) {
1150+
Map<String, Object> statementTime = (Map<String, Object>) context.getVariable("$statementTime");
1151+
if (statementTime == null) {
1152+
// First call - freeze the current time
1153+
statementTime = new java.util.HashMap<>();
1154+
statementTime.put("date", CypherDate.now());
1155+
statementTime.put("localtime", CypherLocalTime.now());
1156+
statementTime.put("time", CypherTime.now());
1157+
statementTime.put("localdatetime", CypherLocalDateTime.now());
1158+
statementTime.put("datetime", CypherDateTime.now());
1159+
context.setVariable("$statementTime", statementTime);
1160+
}
1161+
return statementTime;
1162+
}
1163+
11421164
@SuppressWarnings("unchecked")
11431165
private static class DateConstructorFunction implements StatelessFunction {
11441166
@Override public String getName() { return "date"; }
11451167
@Override public Object execute(final Object[] args, final CommandContext context) {
11461168
if (args.length == 0)
1147-
return CypherDate.now();
1169+
return getStatementTime(context).get("date");
1170+
if (args[0] == null)
1171+
return null;
11481172
if (args[0] instanceof String)
11491173
return CypherDate.parse((String) args[0]);
11501174
if (args[0] instanceof Map)
@@ -1177,7 +1201,9 @@ private static class LocalTimeConstructorFunction implements StatelessFunction {
11771201
@Override public String getName() { return "localtime"; }
11781202
@Override public Object execute(final Object[] args, final CommandContext context) {
11791203
if (args.length == 0)
1180-
return CypherLocalTime.now();
1204+
return getStatementTime(context).get("localtime");
1205+
if (args[0] == null)
1206+
return null;
11811207
if (args[0] instanceof String)
11821208
return CypherLocalTime.parse((String) args[0]);
11831209
if (args[0] instanceof Map)
@@ -1201,7 +1227,9 @@ private static class TimeConstructorFunction implements StatelessFunction {
12011227
@Override public String getName() { return "time"; }
12021228
@Override public Object execute(final Object[] args, final CommandContext context) {
12031229
if (args.length == 0)
1204-
return CypherTime.now();
1230+
return getStatementTime(context).get("time");
1231+
if (args[0] == null)
1232+
return null;
12051233
if (args[0] instanceof String)
12061234
return CypherTime.parse((String) args[0]);
12071235
if (args[0] instanceof Map)
@@ -1223,7 +1251,9 @@ private static class LocalDateTimeConstructorFunction implements StatelessFuncti
12231251
@Override public String getName() { return "localdatetime"; }
12241252
@Override public Object execute(final Object[] args, final CommandContext context) {
12251253
if (args.length == 0)
1226-
return CypherLocalDateTime.now();
1254+
return getStatementTime(context).get("localdatetime");
1255+
if (args[0] == null)
1256+
return null;
12271257
if (args[0] instanceof String)
12281258
return CypherLocalDateTime.parse((String) args[0]);
12291259
if (args[0] instanceof Map)
@@ -1249,7 +1279,9 @@ private static class DateTimeConstructorFunction implements StatelessFunction {
12491279
@Override public String getName() { return "datetime"; }
12501280
@Override public Object execute(final Object[] args, final CommandContext context) {
12511281
if (args.length == 0)
1252-
return CypherDateTime.now();
1282+
return getStatementTime(context).get("datetime");
1283+
if (args[0] == null)
1284+
return null;
12531285
if (args[0] instanceof String)
12541286
return CypherDateTime.parse((String) args[0]);
12551287
if (args[0] instanceof Map)
@@ -1274,6 +1306,8 @@ private static class DurationConstructorFunction implements StatelessFunction {
12741306
@Override public Object execute(final Object[] args, final CommandContext context) {
12751307
if (args.length != 1)
12761308
throw new CommandExecutionException("duration() requires exactly one argument");
1309+
if (args[0] == null)
1310+
return null;
12771311
if (args[0] instanceof String)
12781312
return CypherDuration.parse((String) args[0]);
12791313
if (args[0] instanceof Map)
@@ -1359,9 +1393,14 @@ else if (args[1] instanceof CypherLocalDateTime)
13591393
else
13601394
throw new CommandExecutionException("time.truncate() second argument must be a temporal value with a time");
13611395
LocalTime truncated = TemporalUtil.truncateLocalTime(time.toLocalTime(), unit);
1362-
if (args.length >= 3 && args[2] instanceof Map)
1363-
truncated = applyTimeMap(truncated, (Map<String, Object>) args[2]);
1364-
return new CypherTime(OffsetTime.of(truncated, time.getOffset()));
1396+
ZoneOffset offset = time.getOffset();
1397+
if (args.length >= 3 && args[2] instanceof Map) {
1398+
final Map<String, Object> adjustMap = (Map<String, Object>) args[2];
1399+
truncated = applyTimeMap(truncated, adjustMap);
1400+
if (adjustMap.containsKey("timezone"))
1401+
offset = TemporalUtil.parseOffset(adjustMap.get("timezone").toString());
1402+
}
1403+
return new CypherTime(OffsetTime.of(truncated, offset));
13651404
}
13661405
}
13671406

@@ -1413,9 +1452,14 @@ else if (args[1] instanceof LocalDate)
14131452
else
14141453
throw new CommandExecutionException("datetime.truncate() second argument must be a temporal value");
14151454
LocalDateTime truncated = TemporalUtil.truncateLocalDateTime(dt.toLocalDateTime(), unit);
1416-
if (args.length >= 3 && args[2] instanceof Map)
1417-
truncated = applyDateTimeMap(truncated, (Map<String, Object>) args[2]);
1418-
return new CypherDateTime(ZonedDateTime.of(truncated, dt.getZone()));
1455+
ZoneId zone = dt.getZone();
1456+
if (args.length >= 3 && args[2] instanceof Map) {
1457+
final Map<String, Object> adjustMap = (Map<String, Object>) args[2];
1458+
truncated = applyDateTimeMap(truncated, adjustMap);
1459+
if (adjustMap.containsKey("timezone"))
1460+
zone = TemporalUtil.parseZone(adjustMap.get("timezone").toString());
1461+
}
1462+
return new CypherDateTime(ZonedDateTime.of(truncated, zone));
14191463
}
14201464
}
14211465

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,14 +314,32 @@ else if (value instanceof Expression) {
314314

315315
/**
316316
* Convert CypherTemporalValue objects to java.time types for ArcadeDB storage.
317+
* Handles both single values and collections/arrays of temporal values.
317318
*/
318319
private static Object convertTemporalForStorage(final Object value) {
320+
// Handle collections (lists/arrays of temporal values)
321+
if (value instanceof java.util.Collection<?> collection) {
322+
final java.util.List<Object> converted = new java.util.ArrayList<>(collection.size());
323+
for (final Object item : collection) {
324+
converted.add(convertTemporalForStorage(item));
325+
}
326+
return converted;
327+
}
328+
if (value instanceof Object[] array) {
329+
final Object[] converted = new Object[array.length];
330+
for (int i = 0; i < array.length; i++) {
331+
converted[i] = convertTemporalForStorage(array[i]);
332+
}
333+
return converted;
334+
}
335+
336+
// Handle single temporal values
319337
if (value instanceof CypherDate)
320338
return ((CypherDate) value).getValue();
321339
if (value instanceof CypherLocalDateTime)
322340
return ((CypherLocalDateTime) value).getValue();
323341
if (value instanceof CypherDateTime)
324-
return ((CypherDateTime) value).getValue().toLocalDateTime();
342+
return value.toString(); // Store as String to preserve timezone info
325343
if (value instanceof CypherLocalTime)
326344
return ((CypherLocalTime) value).getValue().toString();
327345
if (value instanceof CypherTime)

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

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import com.arcadedb.query.sql.executor.Result;
3232
import com.arcadedb.query.sql.executor.ResultSet;
3333

34+
import java.time.LocalDate;
35+
import java.time.LocalDateTime;
3436
import java.util.ArrayList;
3537
import java.util.Comparator;
3638
import java.util.List;
@@ -134,12 +136,79 @@ private Object extractValue(final Result result, final OrderByClause.OrderByItem
134136
return null;
135137

136138
if (obj instanceof Vertex)
137-
return ((Vertex) obj).get(parts[1]);
139+
return convertFromStorage(((Vertex) obj).get(parts[1]));
138140
else if (obj instanceof Edge)
139-
return ((Edge) obj).get(parts[1]);
141+
return convertFromStorage(((Edge) obj).get(parts[1]));
140142
}
141143

142-
return result.getProperty(expression);
144+
return convertFromStorage(result.getProperty(expression));
145+
}
146+
147+
/**
148+
* Convert ArcadeDB-stored values back to Cypher temporal types for proper comparison.
149+
* Duration, LocalTime, and Time are stored as Strings because ArcadeDB
150+
* doesn't have native binary types for them.
151+
*/
152+
private static Object convertFromStorage(final Object value) {
153+
// Handle collections (lists/arrays of temporal values)
154+
if (value instanceof java.util.Collection<?> collection) {
155+
final java.util.List<Object> converted = new java.util.ArrayList<>(collection.size());
156+
for (final Object item : collection) {
157+
converted.add(convertFromStorage(item));
158+
}
159+
return converted;
160+
}
161+
if (value instanceof Object[] array) {
162+
final Object[] converted = new Object[array.length];
163+
for (int i = 0; i < array.length; i++) {
164+
converted[i] = convertFromStorage(array[i]);
165+
}
166+
return converted;
167+
}
168+
169+
// Handle single values
170+
if (value instanceof LocalDate ld)
171+
return new CypherDate(ld);
172+
if (value instanceof LocalDateTime ldt)
173+
return new CypherLocalDateTime(ldt);
174+
if (value instanceof String str) {
175+
// Duration strings start with P (ISO-8601)
176+
if (str.length() > 1 && str.charAt(0) == 'P') {
177+
try {
178+
return CypherDuration.parse(str);
179+
} catch (final Exception ignored) {
180+
// Not a valid duration string
181+
}
182+
}
183+
// DateTime strings: contain 'T' with date part before it and timezone/offset
184+
final int tIdx = str.indexOf('T');
185+
if (tIdx >= 4 && tIdx < str.length() - 1 && Character.isDigit(str.charAt(0))) {
186+
try {
187+
return CypherDateTime.parse(str);
188+
} catch (final Exception ignored) {
189+
// Not a valid datetime string
190+
}
191+
}
192+
// Time strings: HH:MM:SS[.nanos][+/-offset] or HH:MM:SS[.nanos]Z
193+
if (str.length() >= 8 && str.charAt(2) == ':' && str.charAt(5) == ':') {
194+
// Check if it has a timezone offset
195+
final boolean hasOffset = str.contains("+") || str.contains("-") || str.endsWith("Z");
196+
if (hasOffset) {
197+
try {
198+
return CypherTime.parse(str);
199+
} catch (final Exception ignored) {
200+
// Not a valid time string
201+
}
202+
} else {
203+
try {
204+
return CypherLocalTime.parse(str);
205+
} catch (final Exception ignored) {
206+
// Not a valid local time string
207+
}
208+
}
209+
}
210+
}
211+
return value;
143212
}
144213

145214
/**

engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherASTBuilder.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,8 +1076,11 @@ public Map<String, Object> visitMap(final Cypher25Parser.MapContext ctx) {
10761076
final ParameterExpression paramExpr = (ParameterExpression) expr;
10771077
value = new ParameterReference(paramExpr.getParameterName());
10781078
} else if (expr instanceof ListExpression) {
1079-
// Evaluate list literals immediately
1080-
value = expr.evaluate(null, null);
1079+
// Evaluate list literals immediately, but only if all elements are simple literals
1080+
if (isStaticListExpression((ListExpression) expr))
1081+
value = expr.evaluate(null, null);
1082+
else
1083+
value = expr; // Keep as Expression for runtime evaluation (e.g., [date({...})])
10811084
} else {
10821085
// Keep dynamic expressions as Expression objects for runtime evaluation
10831086
value = expr;
@@ -1089,6 +1092,14 @@ public Map<String, Object> visitMap(final Cypher25Parser.MapContext ctx) {
10891092
return map;
10901093
}
10911094

1095+
private static boolean isStaticListExpression(final ListExpression listExpr) {
1096+
for (final Expression element : listExpr.getElements()) {
1097+
if (!(element instanceof LiteralExpression))
1098+
return false;
1099+
}
1100+
return true;
1101+
}
1102+
10921103
private List<String> extractLabels(final Cypher25Parser.LabelExpressionContext ctx) {
10931104
// Delegate to ParserUtils for grammar-based label extraction
10941105
return ParserUtils.extractLabels(ctx);

0 commit comments

Comments
 (0)