Skip to content

Fix 500 from coalesce, mvappend, array and percentile_approx types on short, byte and float values - #5874

Open
cnoramut wants to merge 1 commit into
opensearch-project:mainfrom
cnoramut:fix/5873-numeric-udf-result-class
Open

cnoramut wants to merge 1 commit into
opensearch-project:mainfrom
cnoramut:fix/5873-numeric-udf-result-class

Conversation

@cnoramut

Copy link
Copy Markdown
Contributor

Description

coalesce, mvappend, array and percentile_approx return HTTP 500 with a ClassCastException for some numeric types. coalesce(s) on a short or byte field fails, and so does any of the first three with a float literal such as 2.5F. percentile_approx and median fail in memory on short, byte, float and half_float fields.

Root cause. Each function hands its result to Calcite as an Object, and Calcite casts it to the Java class of the declared type. That class is Short for SMALLINT, Byte for TINYINT, Float for REAL and Double for FLOAT, since Calcite's FLOAT is double precision per its data types. Each function built the wrong class for some of these.

// EnhancedCoalesceFunction.coerceToType
case "SMALLINT", "TINYINT" ->
    tryConvert(() -> ExprValueUtils.integerValue(value.integerValue()), value);
case "FLOAT", "REAL" ->
    tryConvert(() -> ExprValueUtils.floatValue(value.floatValue()), value);

mvappend and array built Float elements for FLOAT in the same way. percentile_approx.result returned a double for SMALLINT, TINYINT and REAL and a float for FLOAT. A float field is typed REAL, so it reaches FLOAT only next to a literal such as 2.5F.

Fix. Each switch now builds the class Calcite expects. SMALLINT and TINYINT build a short and a byte, FLOAT joins DOUBLE, and REAL keeps the float.

Before and after. HTTP status on one index with s mapped short, b mapped byte and f mapped float, on main at 00262797c and on this branch.

Query Before After
eval x = coalesce(s) 500, Integer cannot be cast to Short 200, smallint
eval x = coalesce(b) 500, Integer cannot be cast to Byte 200, tinyint
eval x = coalesce(f, 2.5F) 500, Float cannot be cast to Double 200, float
eval x = mvappend(f, 2.5F) 500, Float cannot be cast to Double 200, array
eval x = array(f, 2.5F) 500, Float cannot be cast to Double 200, array
stats percentile_approx(s, 50), max(lower(host)) 500, Double cannot be cast to Short 200, smallint
stats median(s), max(lower(host)) 500, Double cannot be cast to Short 200, smallint
stats percentile_approx(f, 50), max(lower(host)) 500, Double cannot be cast to Float 200, float
eval x = coalesce(f) 200, float 200, float

A matrix crossed every numeric field type with each literal kind and every field pair. On main, 33 of 126 coalesce calls failed, 15 of 108 mvappend calls, 16 float-literal array calls and 10 of 24 percentile_approx calls. On this branch each of those returns 200, apart from coalesce calls on unsigned_long, which PPL does not support and which fail the same way on main. The fixed values match the first present argument for coalesce, the elements for mvappend and array, and the pushed-down result for percentile_approx.

Notes

  1. Test posture - issues/5873.yml adds 15 cases. The casts happen in generated code and in Avatica, so a plan-level unit test cannot reach them and a YAML REST test can. ArrayFunctionImplTest pinned the old Float result for FLOAT, so it now expects doubles.
  2. Why FLOAT sits with DOUBLE - It looks like a typo next to REAL, but it matches Calcite's own mapping, which ExprSqlType already copies as case DOUBLE, FLOAT. Each switch carries a one-line comment, since regrouping FLOAT with REAL would bring the 500 back. A float field widened to FLOAT shows float noise, so 0.1 becomes 0.10000000149011612, the same as ifnull returns.

Related Issues

Resolves #5873

Testing

  • issues/5873.yml, 15 cases. Without the fix, every case except the coalesce(f) and coalesce(s, 0) controls fails with HTTP 500. Reverting one function's hunk fails only that function's cases.
  • :core:test ran 5334 tests and :ppl:test ran 2145, both with 0 failures.
  • The before and after table and the matrices ran on a local single-node 3.10.0-SNAPSHOT build of main and of this branch.

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • New functionality has javadoc added.
  • New functionality has a user manual doc added.
  • New PPL command checklist all confirmed.
  • API changes companion pull request created.
  • Commits are signed per the DCO using --signoff or -s.
  • Public documentation issue/PR created.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Four functions built a result whose Java class did not match the
class Calcite casts it to for the declared type. coalesce built an
Integer for SMALLINT and TINYINT and a Float for Calcite FLOAT.
mvappend and array built Float elements for Calcite FLOAT.
percentile_approx, which median also uses, returned a Double for
SMALLINT, TINYINT and REAL and a Float for FLOAT. The cast threw, so
the query returned 500 with a ClassCastException.

Build each value with the class Calcite expects. Calcite's FLOAT is
double precision and REAL is single precision, so FLOAT now shares
the DOUBLE arm and REAL keeps the float.

Signed-off-by: Chayanin Noramuttha <cnoramut@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

@github-actions

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Add bounds checking for numeric casts

Direct casting from double to short and byte may cause overflow or precision loss
without validation. Consider adding range checks or documenting the expected
behavior when retValue exceeds the target type's bounds.

core/src/main/java/org/opensearch/sql/calcite/udf/udaf/PercentileApproxFunction.java [61-66]

 case SMALLINT:
-  short shortRet = (short) retValue;
+  short shortRet = (short) Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, retValue));
   return shortRet;
 case TINYINT:
-  byte byteRet = (byte) retValue;
+  byte byteRet = (byte) Math.max(Byte.MIN_VALUE, Math.min(Byte.MAX_VALUE, retValue));
   return byteRet;
Suggestion importance[1-10]: 5

__

Why: The suggestion addresses potential overflow when casting double to short and byte, which is a valid concern. However, the existing code follows the same pattern as the INTEGER and BIGINT cases (lines 55-60), suggesting this is the intended behavior. The suggestion adds defensive programming but may not be necessary if the percentile approximation is expected to stay within bounds. The score reflects a moderate improvement in robustness without being critical.

Low

@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.86%. Comparing base (0026279) to head (8515f96).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...sql/calcite/udf/udaf/PercentileApproxFunction.java 0.00% 5 Missing ⚠️
...nction/udf/condition/EnhancedCoalesceFunction.java 0.00% 4 Missing ⚠️
...xpression/function/CollectionUDF/MVAppendCore.java 0.00% 2 Missing ⚠️
...n/function/CollectionUDF/MVAppendFunctionImpl.java 0.00% 2 Missing ⚠️

❌ Your project check has failed because the head coverage (63.86%) is below the target coverage (99.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #5874      +/-   ##
============================================
+ Coverage     63.80%   63.86%   +0.06%     
- Complexity     9070     9088      +18     
============================================
  Files           962      962              
  Lines         40989    41034      +45     
  Branches       4660     4668       +8     
============================================
+ Hits          26152    26206      +54     
+ Misses        13960    13950      -10     
- Partials        877      878       +1     
Flag Coverage Δ
sql-engine 63.86% <0.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] coalesce, mvappend, array and percentile_approx return 500 ClassCastException for short, byte, float and float literal values

1 participant