Skip to content

Commit d2b0a9b

Browse files
authored
Merge pull request #449 from InseeFr/fix/comparison
Fix comparison type issue (Fix #447)
2 parents 8630031 + 4eda897 commit d2b0a9b

File tree

4 files changed

+120
-6
lines changed

4 files changed

+120
-6
lines changed

vtl-engine/src/main/java/fr/insee/vtl/engine/utils/TypeChecking.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static fr.insee.vtl.engine.VtlScriptEngine.fromContext;
44

55
import fr.insee.vtl.engine.exceptions.VtlRuntimeException;
6+
import fr.insee.vtl.model.Dataset;
67
import fr.insee.vtl.model.ResolvableExpression;
78
import fr.insee.vtl.model.TypedExpression;
89
import fr.insee.vtl.model.exceptions.InvalidTypeException;
@@ -119,6 +120,28 @@ public static boolean hasSameTypeOrNull(List<ResolvableExpression> expressions)
119120
<= 1;
120121
}
121122

123+
/**
124+
* Checks if expressions have the same type, all are numbers, or all are null.
125+
*
126+
* @param expressions List of resolvable expressions to check.
127+
* @return A boolean which is <code>true</code> if the expressions have the same type, are all
128+
* numbers, or are all null, <code>false</code> otherwise.
129+
*/
130+
public static boolean hasSameTypeOrNumberOrNull(List<ResolvableExpression> expressions) {
131+
var types =
132+
expressions.stream()
133+
.map(ResolvableExpression::getType)
134+
.filter(
135+
clazz ->
136+
clazz != null && !Object.class.equals(clazz) && !clazz.equals(Dataset.class))
137+
.distinct()
138+
.toList();
139+
140+
return types.isEmpty()
141+
|| types.stream().allMatch(Number.class::isAssignableFrom)
142+
|| types.size() == 1; // même type
143+
}
144+
122145
/**
123146
* Checks if an expression evaluates to null. The check is based on the fact that {@link
124147
* TypedExpression#getType()} returns <code>Object</code> if the expression evaluates to null

vtl-engine/src/main/java/fr/insee/vtl/engine/visitors/expression/ComparisonVisitor.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import fr.insee.vtl.engine.exceptions.ConflictingTypesException;
66
import fr.insee.vtl.engine.exceptions.VtlRuntimeException;
7+
import fr.insee.vtl.engine.utils.TypeChecking;
78
import fr.insee.vtl.engine.visitors.expression.functions.GenericFunctionsVisitor;
89
import fr.insee.vtl.model.ListExpression;
910
import fr.insee.vtl.model.Positioned;
@@ -12,11 +13,7 @@
1213
import fr.insee.vtl.model.exceptions.VtlScriptException;
1314
import fr.insee.vtl.parser.VtlBaseVisitor;
1415
import fr.insee.vtl.parser.VtlParser;
15-
import java.util.Date;
16-
import java.util.List;
17-
import java.util.Map;
18-
import java.util.Objects;
19-
import java.util.Set;
16+
import java.util.*;
2017
import java.util.stream.Collectors;
2118
import org.antlr.v4.runtime.Token;
2219
import org.antlr.v4.runtime.tree.TerminalNode;
@@ -138,7 +135,13 @@ public ResolvableExpression visitComparisonExpr(VtlParser.ComparisonExprContext
138135
try {
139136
Token type = ((TerminalNode) ctx.op.getChild(0)).getSymbol();
140137
var leftExpression = exprVisitor.visit(ctx.left);
141-
List<ResolvableExpression> parameters = List.of(leftExpression, exprVisitor.visit(ctx.right));
138+
var rightExpression = exprVisitor.visit(ctx.right);
139+
List<ResolvableExpression> parameters = List.of(leftExpression, rightExpression);
140+
// Check 2 parameters have the same types
141+
if (!TypeChecking.hasSameTypeOrNumberOrNull(parameters)) {
142+
var types = List.of(leftExpression.getType(), rightExpression.getType());
143+
throw new ConflictingTypesException(types, fromContext(ctx));
144+
}
142145
// If a parameter is the null token
143146
if (parameters.stream().map(TypedExpression::getType).anyMatch(Object.class::equals)) {
144147
return ResolvableExpression.withType(Boolean.class)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package fr.insee.vtl.engine.utils;
2+
3+
import static org.junit.jupiter.api.Assertions.assertFalse;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import fr.insee.vtl.model.Positioned;
7+
import fr.insee.vtl.model.ResolvableExpression;
8+
import java.util.List;
9+
import org.junit.jupiter.api.Test;
10+
11+
public class TypeCheckingUtilsTest {
12+
13+
Positioned fakePos =
14+
new Positioned() {
15+
@Override
16+
public Position getPosition() {
17+
return new Position(1, 1, 1, 1);
18+
}
19+
};
20+
21+
private ResolvableExpression expr(Class<?> type) {
22+
return ResolvableExpression.withType(type)
23+
// return fakePos, value is not read
24+
.withPosition(fakePos)
25+
// return always null, value is not read
26+
.using(c -> null);
27+
}
28+
29+
@Test
30+
void testAllNull() {
31+
var list = List.of(expr(null), expr(null));
32+
assertTrue(TypeChecking.hasSameTypeOrNumberOrNull(list));
33+
}
34+
35+
@Test
36+
void testAllSameType() {
37+
var list = List.of(expr(String.class), expr(String.class));
38+
assertTrue(TypeChecking.hasSameTypeOrNumberOrNull(list));
39+
}
40+
41+
@Test
42+
void testDifferentTypes() {
43+
var list = List.of(expr(String.class), expr(Integer.class));
44+
assertFalse(TypeChecking.hasSameTypeOrNumberOrNull(list));
45+
}
46+
47+
@Test
48+
void testAllNumbersSameSubclass() {
49+
var list = List.of(expr(Integer.class), expr(Integer.class));
50+
assertTrue(TypeChecking.hasSameTypeOrNumberOrNull(list));
51+
}
52+
53+
@Test
54+
void testAllNumbersDifferentSubclass() {
55+
var list = List.of(expr(Integer.class), expr(Double.class), expr(Long.class));
56+
assertTrue(TypeChecking.hasSameTypeOrNumberOrNull(list));
57+
}
58+
59+
@Test
60+
void testMixNullAndNumbers() {
61+
var list = List.of(expr(null), expr(Integer.class), expr(Double.class));
62+
assertTrue(TypeChecking.hasSameTypeOrNumberOrNull(list));
63+
}
64+
65+
@Test
66+
void testMixNullAndOtherType() {
67+
var list = List.of(expr(null), expr(String.class));
68+
assertTrue(TypeChecking.hasSameTypeOrNumberOrNull(list));
69+
}
70+
}

vtl-spark/src/test/java/fr/insee/vtl/spark/processing.engine/FilterTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package fr.insee.vtl.spark.processing.engine;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
45

56
import fr.insee.vtl.engine.VtlScriptEngine;
7+
import fr.insee.vtl.engine.exceptions.ConflictingTypesException;
68
import fr.insee.vtl.model.Dataset;
79
import fr.insee.vtl.model.InMemoryDataset;
810
import fr.insee.vtl.model.Structured;
@@ -53,6 +55,22 @@ public void tearDown() {
5355
if (spark != null) spark.close();
5456
}
5557

58+
@Test
59+
public void testLessThanBadTypes() {
60+
ScriptContext context = engine.getContext();
61+
context.setAttribute("ds", dataset, ScriptContext.ENGINE_SCOPE);
62+
assertThatThrownBy(() -> engine.eval("ds_out := ds[filter name < 1];"))
63+
.isInstanceOf(ConflictingTypesException.class)
64+
.hasMessage("conflicting types: [String, Long]");
65+
}
66+
67+
@Test
68+
public void testLessThanGoodTypes() throws ScriptException {
69+
ScriptContext context = engine.getContext();
70+
context.setAttribute("ds", dataset, ScriptContext.ENGINE_SCOPE);
71+
engine.eval("ds_out := ds[filter 18.1 < age];");
72+
}
73+
5674
@Test
5775
public void testFilterClause() throws ScriptException {
5876

0 commit comments

Comments
 (0)