Skip to content

Commit 811d818

Browse files
committed
fix: fixed many issues with opencypher from TCK tests
1 parent 348eb46 commit 811d818

File tree

13 files changed

+265
-138
lines changed

13 files changed

+265
-138
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ public interface BooleanExpression {
3535
*/
3636
boolean evaluate(Result result, CommandContext context);
3737

38+
/**
39+
* Evaluate with three-valued logic (true/false/null).
40+
* Returns Boolean.TRUE, Boolean.FALSE, or null for unknown.
41+
* Default implementation delegates to evaluate() for backward compatibility.
42+
*/
43+
default Object evaluateTernary(Result result, CommandContext context) {
44+
return evaluate(result, context);
45+
}
46+
3847
/**
3948
* Get the text representation of this expression.
4049
*

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public BooleanWrapperExpression(final BooleanExpression booleanExpression) {
3535

3636
@Override
3737
public Object evaluate(final Result result, final CommandContext context) {
38-
return booleanExpression.evaluate(result, context);
38+
return booleanExpression.evaluateTernary(result, context);
3939
}
4040

4141
@Override

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

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,33 +71,30 @@ public ComparisonExpression(final Expression left, final Operator operator, fina
7171

7272
@Override
7373
public boolean evaluate(final Result result, final CommandContext context) {
74+
final Object ternary = evaluateTernary(result, context);
75+
return Boolean.TRUE.equals(ternary);
76+
}
77+
78+
@Override
79+
public Object evaluateTernary(final Result result, final CommandContext context) {
7480
final Object leftValue;
7581
final Object rightValue;
7682

77-
// Use the shared expression evaluator from OpenCypherQueryEngine (stateless and thread-safe)
78-
// Check if either side is a function call to decide whether to use the evaluator
7983
if (left instanceof FunctionCallExpression || right instanceof FunctionCallExpression) {
80-
// Use ExpressionEvaluator to properly handle function calls
8184
leftValue = OpenCypherQueryEngine.getExpressionEvaluator().evaluate(left, result, context);
8285
rightValue = OpenCypherQueryEngine.getExpressionEvaluator().evaluate(right, result, context);
8386
} else {
84-
// Direct evaluation for simple expressions (optimization)
8587
leftValue = left.evaluate(result, context);
8688
rightValue = right.evaluate(result, context);
8789
}
8890

89-
return compareValues(leftValue, rightValue);
91+
return compareValuesTernary(leftValue, rightValue);
9092
}
9193

92-
private boolean compareValues(final Object left, final Object right) {
93-
// Handle null comparisons
94-
if (left == null || right == null) {
95-
return switch (operator) {
96-
case EQUALS -> left == right;
97-
case NOT_EQUALS -> left != right;
98-
default -> false;
99-
};
100-
}
94+
private Object compareValuesTernary(final Object left, final Object right) {
95+
// In OpenCypher, any comparison involving null returns null
96+
if (left == null || right == null)
97+
return null;
10198

10299
// Numeric comparison
103100
if (left instanceof Number && right instanceof Number) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public ComparisonExpressionWrapper(final Expression left, final ComparisonExpres
3636

3737
@Override
3838
public Object evaluate(final Result result, final CommandContext context) {
39-
return comparison.evaluate(result, context);
39+
return comparison.evaluateTernary(result, context);
4040
}
4141

4242
@Override

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

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,53 +43,56 @@ public InExpression(final Expression expression, final List<Expression> list, fi
4343

4444
@Override
4545
public boolean evaluate(final Result result, final CommandContext context) {
46+
final Object ternary = evaluateTernary(result, context);
47+
return Boolean.TRUE.equals(ternary);
48+
}
49+
50+
@Override
51+
public Object evaluateTernary(final Result result, final CommandContext context) {
4652
final Object value;
4753

48-
// Use the shared expression evaluator from OpenCypherQueryEngine (stateless and thread-safe)
49-
// Check if the expression is a function call to decide whether to use the evaluator
50-
if (expression instanceof FunctionCallExpression) {
51-
// Use ExpressionEvaluator to properly handle function calls
54+
if (expression instanceof FunctionCallExpression)
5255
value = OpenCypherQueryEngine.getExpressionEvaluator().evaluate(expression, result, context);
53-
} else {
54-
// Direct evaluation for simple expressions (optimization)
56+
else
5557
value = expression.evaluate(result, context);
56-
}
5758

5859
// Build the list of values to check against
59-
// This handles both list literals [1,2,3] and parameters $ids where the parameter is a list
6060
final List<Object> valuesToCheck = new ArrayList<>();
6161

6262
for (final Expression listItem : list) {
6363
final Object listValue;
6464

65-
// Similarly, check if list items are function calls
66-
if (listItem instanceof FunctionCallExpression) {
65+
if (listItem instanceof FunctionCallExpression)
6766
listValue = OpenCypherQueryEngine.getExpressionEvaluator().evaluate(listItem, result, context);
68-
} else {
67+
else
6968
listValue = listItem.evaluate(result, context);
70-
}
7169

72-
// If the evaluated value is itself a list/collection (e.g., from a parameter),
73-
// expand it into individual values
74-
if (listValue instanceof List) {
70+
if (listValue instanceof List)
7571
valuesToCheck.addAll((List<?>) listValue);
76-
} else if (listValue instanceof Collection) {
72+
else if (listValue instanceof Collection)
7773
valuesToCheck.addAll((Collection<?>) listValue);
78-
} else {
74+
else
7975
valuesToCheck.add(listValue);
80-
}
8176
}
8277

83-
// Check if value is in the expanded list
84-
boolean found = false;
78+
// 3VL: null IN [1,2,3] -> null, 5 IN [1,null,3] -> null (if not found otherwise)
79+
boolean foundNull = false;
8580
for (final Object checkValue : valuesToCheck) {
86-
if (valuesEqual(value, checkValue)) {
87-
found = true;
88-
break;
89-
}
81+
if (value == null || checkValue == null) {
82+
if (value == null && checkValue == null) {
83+
// null = null is still null in Cypher IN semantics
84+
foundNull = true;
85+
} else {
86+
foundNull = true;
87+
}
88+
} else if (valuesEqual(value, checkValue))
89+
return isNot ? false : true;
9090
}
9191

92-
return isNot ? !found : found;
92+
if (foundNull)
93+
return isNot ? null : null;
94+
95+
return isNot ? true : false;
9396
}
9497

9598
private boolean valuesEqual(final Object a, final Object b) {

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ public class LogicalExpression implements BooleanExpression {
3030
public enum Operator {
3131
AND,
3232
OR,
33-
NOT
33+
NOT,
34+
XOR
3435
}
3536

3637
private final Operator operator;
@@ -49,19 +50,73 @@ public LogicalExpression(final Operator operator, final BooleanExpression operan
4950

5051
@Override
5152
public boolean evaluate(final Result result, final CommandContext context) {
53+
final Object ternary = evaluateTernary(result, context);
54+
return Boolean.TRUE.equals(ternary);
55+
}
56+
57+
@Override
58+
public Object evaluateTernary(final Result result, final CommandContext context) {
5259
return switch (operator) {
53-
case AND -> left.evaluate(result, context) && right.evaluate(result, context);
54-
case OR -> left.evaluate(result, context) || right.evaluate(result, context);
55-
case NOT -> !left.evaluate(result, context);
60+
case AND -> evaluateAnd(result, context);
61+
case OR -> evaluateOr(result, context);
62+
case NOT -> evaluateNot(result, context);
63+
case XOR -> evaluateXor(result, context);
5664
};
5765
}
5866

67+
private Object evaluateAnd(final Result result, final CommandContext context) {
68+
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
69+
final Boolean rightBool = toBoolean(right.evaluateTernary(result, context));
70+
71+
if (Boolean.FALSE.equals(leftBool) || Boolean.FALSE.equals(rightBool))
72+
return false;
73+
if (leftBool == null || rightBool == null)
74+
return null;
75+
return true;
76+
}
77+
78+
private Object evaluateOr(final Result result, final CommandContext context) {
79+
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
80+
final Boolean rightBool = toBoolean(right.evaluateTernary(result, context));
81+
82+
if (Boolean.TRUE.equals(leftBool) || Boolean.TRUE.equals(rightBool))
83+
return true;
84+
if (leftBool == null || rightBool == null)
85+
return null;
86+
return false;
87+
}
88+
89+
private Object evaluateNot(final Result result, final CommandContext context) {
90+
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
91+
if (leftBool == null)
92+
return null;
93+
return !leftBool;
94+
}
95+
96+
private Object evaluateXor(final Result result, final CommandContext context) {
97+
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
98+
final Boolean rightBool = toBoolean(right.evaluateTernary(result, context));
99+
100+
if (leftBool == null || rightBool == null)
101+
return null;
102+
return leftBool ^ rightBool;
103+
}
104+
105+
private static Boolean toBoolean(final Object value) {
106+
if (value == null)
107+
return null;
108+
if (value instanceof Boolean)
109+
return (Boolean) value;
110+
return true;
111+
}
112+
59113
@Override
60114
public String getText() {
61115
return switch (operator) {
62116
case NOT -> "NOT " + left.getText();
63117
case AND -> "(" + left.getText() + " AND " + right.getText() + ")";
64118
case OR -> "(" + left.getText() + " OR " + right.getText() + ")";
119+
case XOR -> "(" + left.getText() + " XOR " + right.getText() + ")";
65120
};
66121
}
67122

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

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,43 +64,32 @@ public boolean isEmpty() {
6464
public static class OrderByItem {
6565
private final String expression;
6666
private final boolean ascending;
67+
private final Expression expressionAST;
6768

68-
/**
69-
* Creates an order by item.
70-
*
71-
* @param expression expression to order by (e.g., "n.name")
72-
* @param ascending true for ASC, false for DESC
73-
*/
7469
public OrderByItem(final String expression, final boolean ascending) {
70+
this(expression, ascending, null);
71+
}
72+
73+
public OrderByItem(final String expression, final boolean ascending, final Expression expressionAST) {
7574
this.expression = expression;
7675
this.ascending = ascending;
76+
this.expressionAST = expressionAST;
7777
}
7878

79-
/**
80-
* Returns the expression to order by.
81-
*
82-
* @return expression
83-
*/
8479
public String getExpression() {
8580
return expression;
8681
}
8782

88-
/**
89-
* Returns true if ascending order.
90-
*
91-
* @return true if ascending
92-
*/
9383
public boolean isAscending() {
9484
return ascending;
9585
}
9686

97-
/**
98-
* Returns true if descending order.
99-
*
100-
* @return true if descending
101-
*/
10287
public boolean isDescending() {
10388
return !ascending;
10489
}
90+
91+
public Expression getExpressionAST() {
92+
return expressionAST;
93+
}
10594
}
10695
}

0 commit comments

Comments
 (0)