diff --git a/core/src/main/java/org/opensearch/sql/calcite/udf/udaf/PercentileApproxFunction.java b/core/src/main/java/org/opensearch/sql/calcite/udf/udaf/PercentileApproxFunction.java index 613291dcce1..edd3b749b54 100644 --- a/core/src/main/java/org/opensearch/sql/calcite/udf/udaf/PercentileApproxFunction.java +++ b/core/src/main/java/org/opensearch/sql/calcite/udf/udaf/PercentileApproxFunction.java @@ -58,7 +58,16 @@ public Object result(PencentileApproAccumulator acc) { case BIGINT: long longRet = (long) retValue; return longRet; - case FLOAT: + case SMALLINT: + short shortRet = (short) retValue; + return shortRet; + case TINYINT: + byte byteRet = (byte) retValue; + return byteRet; + // Calcite's FLOAT is double precision, and REAL is single precision + case DOUBLE, FLOAT: + return retValue; + case REAL: float floatRet = (float) retValue; return floatRet; default: diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImpl.java index 318f32a41be..86960333c7b 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImpl.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImpl.java @@ -119,18 +119,13 @@ public static Object internalCast(Object... args) { }) .collect(Collectors.toList()); break; - case DOUBLE: + // Calcite's FLOAT is double precision, and REAL is single precision + case DOUBLE, FLOAT: result = originalList.stream() .map(i -> i == null ? null : (Object) ((Number) i).doubleValue()) .collect(Collectors.toList()); break; - case FLOAT: - result = - originalList.stream() - .map(i -> i == null ? null : (Object) ((Number) i).floatValue()) - .collect(Collectors.toList()); - break; case VARCHAR, CHAR: result = originalList.stream() diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendCore.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendCore.java index c37189cb20b..a98d8c900c3 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendCore.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendCore.java @@ -67,8 +67,9 @@ private static Object coerce(Object value, SqlTypeName elementType) { case SMALLINT -> num.shortValue(); case INTEGER -> num.intValue(); case BIGINT -> num.longValue(); - case FLOAT, REAL -> num.floatValue(); - case DOUBLE -> num.doubleValue(); + // Calcite's FLOAT is double precision, and REAL is single precision + case DOUBLE, FLOAT -> num.doubleValue(); + case REAL -> num.floatValue(); case DECIMAL -> num instanceof BigDecimal ? num : BigDecimal.valueOf(num.doubleValue()); default -> value; }; diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java index dc3402e464d..505518db8d1 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVAppendFunctionImpl.java @@ -159,8 +159,9 @@ private static Class boxedJavaClass(SqlTypeName sqlType) { case SMALLINT -> Short.class; case INTEGER -> Integer.class; case BIGINT -> Long.class; - case FLOAT, REAL -> Float.class; - case DOUBLE -> Double.class; + // Calcite's FLOAT is double precision, and REAL is single precision + case DOUBLE, FLOAT -> Double.class; + case REAL -> Float.class; case DECIMAL -> BigDecimal.class; case CHAR, VARCHAR -> String.class; default -> Object.class; diff --git a/core/src/main/java/org/opensearch/sql/expression/function/udf/condition/EnhancedCoalesceFunction.java b/core/src/main/java/org/opensearch/sql/expression/function/udf/condition/EnhancedCoalesceFunction.java index c6ff1a64478..cebbf20331e 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/udf/condition/EnhancedCoalesceFunction.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/udf/condition/EnhancedCoalesceFunction.java @@ -63,11 +63,12 @@ private static ExprValue coerceToType(ExprValue value, String typeName) { return switch (typeName) { case "INTEGER" -> tryConvert(() -> ExprValueUtils.integerValue(value.integerValue()), value); case "BIGINT" -> tryConvert(() -> ExprValueUtils.longValue(value.longValue()), value); - case "SMALLINT", "TINYINT" -> - tryConvert(() -> ExprValueUtils.integerValue(value.integerValue()), value); - case "DOUBLE" -> tryConvert(() -> ExprValueUtils.doubleValue(value.doubleValue()), value); - case "FLOAT", "REAL" -> - tryConvert(() -> ExprValueUtils.floatValue(value.floatValue()), value); + case "SMALLINT" -> tryConvert(() -> ExprValueUtils.shortValue(value.shortValue()), value); + case "TINYINT" -> tryConvert(() -> ExprValueUtils.byteValue(value.byteValue()), value); + // Calcite's FLOAT is double precision, and REAL is single precision + case "DOUBLE", "FLOAT" -> + tryConvert(() -> ExprValueUtils.doubleValue(value.doubleValue()), value); + case "REAL" -> tryConvert(() -> ExprValueUtils.floatValue(value.floatValue()), value); case "BOOLEAN" -> tryConvert(() -> ExprValueUtils.booleanValue(value.booleanValue()), value); case "VARCHAR", "CHAR" -> tryConvert(() -> ExprValueUtils.stringValue(String.valueOf(value.value())), value); diff --git a/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImplTest.java b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImplTest.java index 600a802615a..47aadb06299 100644 --- a/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImplTest.java +++ b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayFunctionImplTest.java @@ -281,9 +281,10 @@ public void testArrayWithFloatTypePreservesNulls() { assertNotNull(result); List list = (List) result; assertEquals(3, list.size()); - assertEquals(1.5f, list.get(0)); + // Calcite FLOAT is double precision, so float elements widen to double + assertEquals(1.5, list.get(0)); assertNull(list.get(1), "Null should be preserved during FLOAT type conversion"); - assertEquals(2.7f, list.get(2)); + assertEquals((double) 2.7f, list.get(2)); } @Test diff --git a/integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/5873.yml b/integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/5873.yml new file mode 100644 index 00000000000..0d55c7fb613 --- /dev/null +++ b/integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/5873.yml @@ -0,0 +1,280 @@ +# Issue: https://github.com/opensearch-project/sql/issues/5873 +# coalesce built its SMALLINT and TINYINT result as an Integer and its Calcite FLOAT result as a +# Float, mvappend and array built Calcite FLOAT elements as Float, and percentile_approx returned +# a Double for SMALLINT, TINYINT and Calcite REAL and a Float for Calcite FLOAT. The code Calcite +# generates, or Avatica's array accessor, then cast them to the declared class, so the query +# returned 500 with a ClassCastException. Expected coalesce values match ifnull on the same field. + +setup: + - do: + query.settings: + body: + transient: + plugins.calcite.enabled : true + plugins.calcite.pushdown.enabled : true + - do: + indices.create: + index: numeric_udf_result_5873 + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + host: + type: keyword + s: + type: short + b: + type: byte + f: + type: float + - do: + bulk: + index: numeric_udf_result_5873 + refresh: true + body: + - '{"index": {}}' + - '{"host": "a", "s": 5, "b": 5, "f": 1.5}' + - '{"index": {}}' + - '{"host": "b"}' + +--- +teardown: + - do: + indices.delete: + index: numeric_udf_result_5873 + - do: + query.settings: + body: + transient: + plugins.calcite.enabled : null + plugins.calcite.pushdown.enabled : null + +--- +"coalesce on a short field returns smallint": + - skip: + features: + - headers + # Before the fix: status 500, Integer cannot be cast to Short + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = coalesce(s) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "smallint"}]} + - match: {"datarows": [["a", 5], ["b", null]]} + +--- +"coalesce on a byte field returns tinyint": + - skip: + features: + - headers + # Before the fix: status 500, Integer cannot be cast to Byte + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = coalesce(b) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "tinyint"}]} + - match: {"datarows": [["a", 5], ["b", null]]} + +--- +"coalesce on a byte and a short field returns smallint": + - skip: + features: + - headers + # Before the fix: status 500, Integer cannot be cast to Short + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = coalesce(b, s) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "smallint"}]} + - match: {"datarows": [["a", 5], ["b", null]]} + +--- +"coalesce on a short field in a pushed filter matches the row": + - skip: + features: + - headers + # Before the fix: status 500, the filter script failed on the shard + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | where coalesce(s) = 5 | fields host + - match: {"datarows": [["a"]]} + +--- +"coalesce on a float field and a float literal returns float": + - skip: + features: + - headers + # Before the fix: status 500, Float cannot be cast to Double + # A float field is Calcite REAL, so the float literal is what makes the result Calcite FLOAT + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = coalesce(f, 2.5F) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "float"}]} + - match: {"datarows": [["a", 1.5], ["b", 2.5]]} + +--- +"coalesce on a float field returns float": + - skip: + features: + - headers + # Control, a float field alone stays Calcite REAL and passed before the fix + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = coalesce(f) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "float"}]} + - match: {"datarows": [["a", 1.5], ["b", null]]} + +--- +"mvappend on a float field and a float literal returns both values": + - skip: + features: + - headers + # Before the fix: status 500, Float cannot be cast to Double + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = mvappend(f, 2.5F) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "array"}]} + - match: {"datarows": [["a", [1.5, 2.5]], ["b", [2.5]]]} + +--- +"mvappend on an array of a float field and a float literal returns both values": + - skip: + features: + - headers + # Before the fix: status 500, Float cannot be cast to Double + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = mvappend(array(f), 2.5F) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "array"}]} + - match: {"datarows": [["a", [1.5, 2.5]], ["b", [2.5]]]} + +--- +"array on a float field and a float literal returns both values": + - skip: + features: + - headers + # Before the fix: status 500, Float cannot be cast to Double + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = array(f, 2.5F) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "array"}]} + - match: {"datarows": [["a", [1.5, 2.5]], ["b", [null, 2.5]]]} + +--- +"percentile_approx on a short field in memory returns smallint": + - skip: + features: + - headers + # Before the fix: status 500, Double cannot be cast to Short + # max(lower(host)) cannot be pushed down, so the percentile runs in memory + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | stats percentile_approx(s, 50) as p, max(lower(host)) as m + - match: {"schema": [{"name": "p", "type": "smallint"}, {"name": "m", "type": "string"}]} + - match: {"datarows": [[5, "b"]]} + +--- +"percentile_approx on a float field in memory returns float": + - skip: + features: + - headers + # Before the fix: status 500, Double cannot be cast to Float + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | stats percentile_approx(f, 50) as p, max(lower(host)) as m + - match: {"schema": [{"name": "p", "type": "float"}, {"name": "m", "type": "string"}]} + - match: {"datarows": [[1.5, "b"]]} + +--- +"percentile_approx on a byte field in memory returns tinyint": + - skip: + features: + - headers + # Before the fix: status 500, Double cannot be cast to Byte + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | stats percentile_approx(b, 50) as p, max(lower(host)) as m + - match: {"schema": [{"name": "p", "type": "tinyint"}, {"name": "m", "type": "string"}]} + - match: {"datarows": [[5, "b"]]} + +--- +"percentile_approx on a float expression with a float literal in memory returns float": + - skip: + features: + - headers + # Before the fix: status 500, Float cannot be cast to Double + # The float literal makes g a Calcite FLOAT + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | eval g = f * 2.0F | stats percentile_approx(g, 50) as p, max(lower(host)) as m + - match: {"schema": [{"name": "p", "type": "float"}, {"name": "m", "type": "string"}]} + - match: {"datarows": [[3.0, "b"]]} + +--- +"median on a short field in memory returns smallint": + - skip: + features: + - headers + # Before the fix: status 500, Double cannot be cast to Short + # median runs through percentile_approx + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | stats median(s) as p, max(lower(host)) as m + - match: {"schema": [{"name": "p", "type": "smallint"}, {"name": "m", "type": "string"}]} + - match: {"datarows": [[5, "b"]]} + +--- +"coalesce on a short field and an int literal returns int": + - skip: + features: + - headers + # Control, the int literal widens the result to int and passed before the fix + - do: + headers: + Content-Type: 'application/json' + ppl: + body: + query: source=numeric_udf_result_5873 | sort host | eval x = coalesce(s, 0) | fields host, x + - match: {"schema": [{"name": "host", "type": "string"}, {"name": "x", "type": "int"}]} + - match: {"datarows": [["a", 5], ["b", 0]]}