From e634b9606f68e353454271634ebcb7d7780af552 Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Wed, 13 May 2026 12:27:47 +0200 Subject: [PATCH] Fix case when input expr is null --- .../expression/ConditionalVisitor.java | 8 +++- .../expression/ConditionalExprTest.java | 2 + .../vtl/spark/processing.engine/CalcTest.java | 37 +++++++++++++++++++ .../spark4/processing.engine/CalcTest.java | 37 +++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/vtl-engine/src/main/java/fr/insee/vtl/engine/visitors/expression/ConditionalVisitor.java b/vtl-engine/src/main/java/fr/insee/vtl/engine/visitors/expression/ConditionalVisitor.java index 686508deb..b10c94acc 100644 --- a/vtl-engine/src/main/java/fr/insee/vtl/engine/visitors/expression/ConditionalVisitor.java +++ b/vtl-engine/src/main/java/fr/insee/vtl/engine/visitors/expression/ConditionalVisitor.java @@ -5,6 +5,7 @@ import fr.insee.vtl.engine.exceptions.VtlRuntimeException; import fr.insee.vtl.engine.visitors.expression.functions.GenericFunctionsVisitor; +import fr.insee.vtl.model.ConstantExpression; import fr.insee.vtl.model.Positioned; import fr.insee.vtl.model.ResolvableExpression; import fr.insee.vtl.model.exceptions.InvalidTypeException; @@ -165,11 +166,14 @@ private ResolvableExpression caseToIfIt( } ResolvableExpression nextWhen = whenExpr.next(); + ResolvableExpression caseCondition = + genericFunctionsVisitor.invokeFunction( + "nvl", List.of(nextWhen, new ConstantExpression(false, nextWhen)), nextWhen); return genericFunctionsVisitor.invokeFunction( "ifThenElse", - List.of(nextWhen, thenExpr.next(), caseToIfIt(whenExpr, thenExpr, elseExpression)), - nextWhen); + List.of(caseCondition, thenExpr.next(), caseToIfIt(whenExpr, thenExpr, elseExpression)), + caseCondition); } /** diff --git a/vtl-engine/src/test/java/fr/insee/vtl/engine/visitors/expression/ConditionalExprTest.java b/vtl-engine/src/test/java/fr/insee/vtl/engine/visitors/expression/ConditionalExprTest.java index 5ce05c628..37dd9efa0 100644 --- a/vtl-engine/src/test/java/fr/insee/vtl/engine/visitors/expression/ConditionalExprTest.java +++ b/vtl-engine/src/test/java/fr/insee/vtl/engine/visitors/expression/ConditionalExprTest.java @@ -69,6 +69,8 @@ public void testCaseExpr() throws ScriptException { assertThat(context.getAttribute("s2")).isEqualTo("yes"); engine.eval("s3 := case when false then \"no\" when 1=2 then \"yes\" else \"else\";"); assertThat(context.getAttribute("s3")).isEqualTo("else"); + engine.eval("s4 := case when cast(null, boolean) then \"no\" else \"else\";"); + assertThat(context.getAttribute("s4")).isEqualTo("else"); engine.getContext().setAttribute("ds_1", DatasetSamples.ds1, ScriptContext.ENGINE_SCOPE); engine.getContext().setAttribute("ds_2", DatasetSamples.ds2, ScriptContext.ENGINE_SCOPE); diff --git a/vtl-spark/src/test/java/fr/insee/vtl/spark/processing.engine/CalcTest.java b/vtl-spark/src/test/java/fr/insee/vtl/spark/processing.engine/CalcTest.java index 0f5ce5d27..933d53ca3 100644 --- a/vtl-spark/src/test/java/fr/insee/vtl/spark/processing.engine/CalcTest.java +++ b/vtl-spark/src/test/java/fr/insee/vtl/spark/processing.engine/CalcTest.java @@ -6,6 +6,7 @@ import fr.insee.vtl.model.Dataset; import fr.insee.vtl.model.InMemoryDataset; import fr.insee.vtl.model.Structured; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.script.ScriptContext; @@ -79,4 +80,40 @@ public void testCalcClause() throws ScriptException, InterruptedException { new Structured.Component("weight", Long.class, Dataset.Role.MEASURE), new Structured.Component("wisdom", Double.class, Dataset.Role.ATTRIBUTE)); } + + @Test + public void testCaseElseAppliesWhenWhenConditionIsNull() throws ScriptException { + ScriptContext context = engine.getContext(); + + Map r4 = new HashMap<>(); + r4.put("name", "NullGuy"); + r4.put("me_1", null); + + InMemoryDataset dsWithNull = + new InMemoryDataset( + List.of( + Map.of("name", "A", "me_1", 0.12D), + Map.of("name", "B", "me_1", 3.5D), + Map.of("name", "C", "me_1", 10.7D), + r4), + Map.of("name", String.class, "me_1", Double.class), + Map.of("name", Dataset.Role.IDENTIFIER, "me_1", Dataset.Role.MEASURE)); + + context.setAttribute("ds_null", dsWithNull, ScriptContext.ENGINE_SCOPE); + engine.eval( + "res := ds_null[calc me_2 := case when me_1 <= 1 then 0 when me_1 > 1 and me_1 <= 10 then 1 when me_1 > 10 then 10 else 100];"); + + Dataset res = (Dataset) context.getAttribute("res"); + assertThat(res.getDataAsMap()) + .contains( + Map.of("name", "A", "me_1", 0.12D, "me_2", 0L), + Map.of("name", "B", "me_1", 3.5D, "me_2", 1L), + Map.of("name", "C", "me_1", 10.7D, "me_2", 10L)); + + Map expectedNullRow = new HashMap<>(); + expectedNullRow.put("name", "NullGuy"); + expectedNullRow.put("me_1", null); + expectedNullRow.put("me_2", 100L); + assertThat(res.getDataAsMap()).contains(expectedNullRow); + } } diff --git a/vtl-spark4/src/test/java/fr/insee/vtl/spark4/processing.engine/CalcTest.java b/vtl-spark4/src/test/java/fr/insee/vtl/spark4/processing.engine/CalcTest.java index 64e6c9f09..b102e271b 100644 --- a/vtl-spark4/src/test/java/fr/insee/vtl/spark4/processing.engine/CalcTest.java +++ b/vtl-spark4/src/test/java/fr/insee/vtl/spark4/processing.engine/CalcTest.java @@ -7,6 +7,7 @@ import fr.insee.vtl.model.InMemoryDataset; import fr.insee.vtl.model.Structured; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; import javax.script.ScriptContext; @@ -80,4 +81,40 @@ public void testCalcClause() throws ScriptException, InterruptedException { new Structured.Component("weight", Long.class, Dataset.Role.MEASURE), new Structured.Component("wisdom", Double.class, Dataset.Role.ATTRIBUTE)); } + + @Test + public void testCaseElseAppliesWhenWhenConditionIsNull() throws ScriptException { + ScriptContext context = engine.getContext(); + + Map r4 = new HashMap<>(); + r4.put("name", "NullGuy"); + r4.put("me_1", null); + + InMemoryDataset dsWithNull = + new InMemoryDataset( + List.of( + Map.of("name", "A", "me_1", 0.12D), + Map.of("name", "B", "me_1", 3.5D), + Map.of("name", "C", "me_1", 10.7D), + r4), + Map.of("name", String.class, "me_1", Double.class), + Map.of("name", Dataset.Role.IDENTIFIER, "me_1", Dataset.Role.MEASURE)); + + context.setAttribute("ds_null", dsWithNull, ScriptContext.ENGINE_SCOPE); + engine.eval( + "res := ds_null[calc me_2 := case when me_1 <= 1 then 0 when me_1 > 1 and me_1 <= 10 then 1 when me_1 > 10 then 10 else 100];"); + + Dataset res = (Dataset) context.getAttribute("res"); + assertThat(res.getDataAsMap()) + .contains( + Map.of("name", "A", "me_1", 0.12D, "me_2", 0L), + Map.of("name", "B", "me_1", 3.5D, "me_2", 1L), + Map.of("name", "C", "me_1", 10.7D, "me_2", 10L)); + + Map expectedNullRow = new HashMap<>(); + expectedNullRow.put("name", "NullGuy"); + expectedNullRow.put("me_1", null); + expectedNullRow.put("me_2", 100L); + assertThat(res.getDataAsMap()).contains(expectedNullRow); + } }