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
43 changes: 43 additions & 0 deletions engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
<buildnumber-maven-plugin.version>3.3.0</buildnumber-maven-plugin.version>
<antlr4.version>4.9.1</antlr4.version>
<antlr4.visitor>true</antlr4.visitor>
<cucumber.version>7.22.1</cucumber.version>
<junit-platform-suite.version>6.0.2</junit-platform-suite.version>
</properties>

<build>
Expand Down Expand Up @@ -91,6 +93,21 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/tck/OpenCypherTCKRunner.java</exclude>
</excludes>
<!-- Exclude Cucumber engine from normal test runs to prevent
auto-discovery of TCK feature files. Run TCK explicitly with:
mvn test -Dtest=OpenCypherTCKRunner -DfailIfNoTests=false -->
<excludeJUnit5Engines>
<excludeJUnit5Engine>cucumber</excludeJUnit5Engine>
</excludeJUnit5Engines>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
Expand Down Expand Up @@ -187,5 +204,31 @@
<version>1.37</version>
<scope>test</scope>
</dependency>

<!-- Cucumber for OpenCypher TCK -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>${junit-platform-suite.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public interface BooleanExpression {
*/
boolean evaluate(Result result, CommandContext context);

/**
* Evaluate with three-valued logic (true/false/null).
* Returns Boolean.TRUE, Boolean.FALSE, or null for unknown.
* Default implementation delegates to evaluate() for backward compatibility.
*/
default Object evaluateTernary(Result result, CommandContext context) {
return evaluate(result, context);
}

/**
* Get the text representation of this expression.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public BooleanWrapperExpression(final BooleanExpression booleanExpression) {

@Override
public Object evaluate(final Result result, final CommandContext context) {
return booleanExpression.evaluate(result, context);
return booleanExpression.evaluateTernary(result, context);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,33 +71,30 @@ public ComparisonExpression(final Expression left, final Operator operator, fina

@Override
public boolean evaluate(final Result result, final CommandContext context) {
final Object ternary = evaluateTernary(result, context);
return Boolean.TRUE.equals(ternary);
}

@Override
public Object evaluateTernary(final Result result, final CommandContext context) {
final Object leftValue;
final Object rightValue;

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

return compareValues(leftValue, rightValue);
return compareValuesTernary(leftValue, rightValue);
}

private boolean compareValues(final Object left, final Object right) {
// Handle null comparisons
if (left == null || right == null) {
return switch (operator) {
case EQUALS -> left == right;
case NOT_EQUALS -> left != right;
default -> false;
};
}
private Object compareValuesTernary(final Object left, final Object right) {
// In OpenCypher, any comparison involving null returns null
if (left == null || right == null)
return null;

// Numeric comparison
if (left instanceof Number && right instanceof Number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public ComparisonExpressionWrapper(final Expression left, final ComparisonExpres

@Override
public Object evaluate(final Result result, final CommandContext context) {
return comparison.evaluate(result, context);
return comparison.evaluateTernary(result, context);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,53 +43,56 @@ public InExpression(final Expression expression, final List<Expression> list, fi

@Override
public boolean evaluate(final Result result, final CommandContext context) {
final Object ternary = evaluateTernary(result, context);
return Boolean.TRUE.equals(ternary);
}

@Override
public Object evaluateTernary(final Result result, final CommandContext context) {
final Object value;

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

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

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

// Similarly, check if list items are function calls
if (listItem instanceof FunctionCallExpression) {
if (listItem instanceof FunctionCallExpression)
listValue = OpenCypherQueryEngine.getExpressionEvaluator().evaluate(listItem, result, context);
} else {
else
listValue = listItem.evaluate(result, context);
}

// If the evaluated value is itself a list/collection (e.g., from a parameter),
// expand it into individual values
if (listValue instanceof List) {
if (listValue instanceof List)
valuesToCheck.addAll((List<?>) listValue);
} else if (listValue instanceof Collection) {
else if (listValue instanceof Collection)
valuesToCheck.addAll((Collection<?>) listValue);
} else {
else
valuesToCheck.add(listValue);
}
}

// Check if value is in the expanded list
boolean found = false;
// 3VL: null IN [1,2,3] -> null, 5 IN [1,null,3] -> null (if not found otherwise)
boolean foundNull = false;
for (final Object checkValue : valuesToCheck) {
if (valuesEqual(value, checkValue)) {
found = true;
break;
}
if (value == null || checkValue == null) {
if (value == null && checkValue == null) {
// null = null is still null in Cypher IN semantics
foundNull = true;
} else {
foundNull = true;
}
} else if (valuesEqual(value, checkValue))
return isNot ? false : true;
}

return isNot ? !found : found;
if (foundNull)
return isNot ? null : null;

return isNot ? true : false;
}

private boolean valuesEqual(final Object a, final Object b) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public class LogicalExpression implements BooleanExpression {
public enum Operator {
AND,
OR,
NOT
NOT,
XOR
}

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

@Override
public boolean evaluate(final Result result, final CommandContext context) {
final Object ternary = evaluateTernary(result, context);
return Boolean.TRUE.equals(ternary);
}

@Override
public Object evaluateTernary(final Result result, final CommandContext context) {
return switch (operator) {
case AND -> left.evaluate(result, context) && right.evaluate(result, context);
case OR -> left.evaluate(result, context) || right.evaluate(result, context);
case NOT -> !left.evaluate(result, context);
case AND -> evaluateAnd(result, context);
case OR -> evaluateOr(result, context);
case NOT -> evaluateNot(result, context);
case XOR -> evaluateXor(result, context);
};
}

private Object evaluateAnd(final Result result, final CommandContext context) {
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
final Boolean rightBool = toBoolean(right.evaluateTernary(result, context));

if (Boolean.FALSE.equals(leftBool) || Boolean.FALSE.equals(rightBool))
return false;
if (leftBool == null || rightBool == null)
return null;
return true;
}

private Object evaluateOr(final Result result, final CommandContext context) {
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
final Boolean rightBool = toBoolean(right.evaluateTernary(result, context));

if (Boolean.TRUE.equals(leftBool) || Boolean.TRUE.equals(rightBool))
return true;
if (leftBool == null || rightBool == null)
return null;
return false;
}

private Object evaluateNot(final Result result, final CommandContext context) {
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
if (leftBool == null)
return null;
return !leftBool;
}

private Object evaluateXor(final Result result, final CommandContext context) {
final Boolean leftBool = toBoolean(left.evaluateTernary(result, context));
final Boolean rightBool = toBoolean(right.evaluateTernary(result, context));

if (leftBool == null || rightBool == null)
return null;
return leftBool ^ rightBool;
}

private static Boolean toBoolean(final Object value) {
if (value == null)
return null;
if (value instanceof Boolean)
return (Boolean) value;
return true;
}

@Override
public String getText() {
return switch (operator) {
case NOT -> "NOT " + left.getText();
case AND -> "(" + left.getText() + " AND " + right.getText() + ")";
case OR -> "(" + left.getText() + " OR " + right.getText() + ")";
case XOR -> "(" + left.getText() + " XOR " + right.getText() + ")";
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,43 +64,32 @@ public boolean isEmpty() {
public static class OrderByItem {
private final String expression;
private final boolean ascending;
private final Expression expressionAST;

/**
* Creates an order by item.
*
* @param expression expression to order by (e.g., "n.name")
* @param ascending true for ASC, false for DESC
*/
public OrderByItem(final String expression, final boolean ascending) {
this(expression, ascending, null);
}

public OrderByItem(final String expression, final boolean ascending, final Expression expressionAST) {
this.expression = expression;
this.ascending = ascending;
this.expressionAST = expressionAST;
}

/**
* Returns the expression to order by.
*
* @return expression
*/
public String getExpression() {
return expression;
}

/**
* Returns true if ascending order.
*
* @return true if ascending
*/
public boolean isAscending() {
return ascending;
}

/**
* Returns true if descending order.
*
* @return true if descending
*/
public boolean isDescending() {
return !ascending;
}

public Expression getExpressionAST() {
return expressionAST;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ private static Boolean toBoolean(final Object value) {
return true;
}

public Operator getOperator() {
return operator;
}

public Expression getLeft() {
return left;
}

public Expression getRight() {
return right;
}

@Override
public boolean isAggregation() {
return false;
Expand Down
Loading
Loading