diff --git a/core/src/main/java/org/opensearch/sql/calcite/ForeachPlanner.java b/core/src/main/java/org/opensearch/sql/calcite/ForeachPlanner.java index d9ef71b2220..25a8ddef642 100644 --- a/core/src/main/java/org/opensearch/sql/calcite/ForeachPlanner.java +++ b/core/src/main/java/org/opensearch/sql/calcite/ForeachPlanner.java @@ -432,9 +432,9 @@ private UnresolvedExpression asArrayExpression( * VARCHAR. * *
For {@code json_array()} calls and string literals the content is visible at plan time;
- * mixed content is rejected. For opaque expressions — typically a field holding JSON text, the
- * primary Splunk use of json_array mode — content is unknowable, so infer from usage: an item
- * placeholder consumed by arithmetic means numeric elements, anything else means strings.
+ * mixed content is rejected. For opaque expressions, typically a field holding JSON text, content
+ * is unknowable, so infer from usage: an item placeholder consumed by arithmetic means numeric
+ * elements, anything else means strings.
*/
private SqlTypeName jsonElementType(
UnresolvedExpression collection, CalcitePlanContext context, Foreach node) {
diff --git a/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/ForeachJsonArrayFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/ForeachJsonArrayFunctionImpl.java
index 28dff66a8ae..df8783a311e 100644
--- a/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/ForeachJsonArrayFunctionImpl.java
+++ b/core/src/main/java/org/opensearch/sql/expression/function/jsonUDF/ForeachJsonArrayFunctionImpl.java
@@ -10,7 +10,6 @@
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
-import java.math.BigDecimal;
import java.util.List;
import java.util.stream.StreamSupport;
import org.apache.calcite.adapter.enumerable.NotNullImplementor;
@@ -20,6 +19,7 @@
import org.apache.calcite.linq4j.tree.Expressions;
import org.apache.calcite.linq4j.tree.Types;
import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.runtime.SqlFunctions;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
@@ -97,14 +97,19 @@ private static Object cast(Object value, SqlTypeName elementType) {
if (value == null) {
return null;
}
- return switch (elementType) {
- case DOUBLE -> ((Number) value).doubleValue();
- case VARCHAR ->
- value instanceof List> || value instanceof java.util.Map, ?>
- ? gson.toJson(value)
- : String.valueOf(value);
- case DECIMAL -> BigDecimal.valueOf(((Number) value).doubleValue());
- default -> value;
- };
+ // Match Calcite SAFE_CAST semantics for runtime values whose type is unknown during planning.
+ try {
+ return switch (elementType) {
+ case DOUBLE -> SqlFunctions.toDouble(value);
+ case VARCHAR ->
+ value instanceof List> || value instanceof java.util.Map, ?>
+ ? gson.toJson(value)
+ : String.valueOf(value);
+ case DECIMAL -> SqlFunctions.toBigDecimal(value);
+ default -> value;
+ };
+ } catch (RuntimeException e) {
+ return null;
+ }
}
}
diff --git a/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ForeachFunctionImplTest.java b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ForeachFunctionImplTest.java
index e3c3724e0aa..5a633a1a9bb 100644
--- a/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ForeachFunctionImplTest.java
+++ b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ForeachFunctionImplTest.java
@@ -40,6 +40,13 @@ public void testMalformedJsonArrayIsEmpty() {
assertEquals(List.of(), ForeachJsonArrayFunctionImpl.eval("not-json", "VARCHAR"));
}
+ @Test
+ public void testJsonArraySafelyCoercesNumericElements() {
+ assertEquals(
+ Arrays.asList(10.0, 20.0, null),
+ ForeachJsonArrayFunctionImpl.eval("[10,\"20\",\"not-a-number\"]", "DOUBLE"));
+ }
+
@Test
public void testStatePreservesHeterogeneousAndNullSlots() {
assertEquals(
diff --git a/docs/user/ppl/cmd/foreach.md b/docs/user/ppl/cmd/foreach.md
index 45dcb939f8a..ba6b758fa5d 100644
--- a/docs/user/ppl/cmd/foreach.md
+++ b/docs/user/ppl/cmd/foreach.md
@@ -41,9 +41,9 @@ Placeholders renamed via `itemstr`/`iterstr` may be written without the `<<...>>
The following considerations apply when using the `foreach` command:
* In collection modes, the bracketed `eval` acts as an accumulator: each target field must already exist (typically initialized with a preceding `eval`), and the expressions are applied once per element. Multiple assignments run from left to right, so a later assignment in the same iteration sees an earlier assignment's updated value.
-* In `json_array` mode the element type is inferred: a `json_array(...)` call or JSON string literal is inspected at plan time; for a field holding JSON text, elements are treated as numbers when `<