Skip to content

Conversation

@ahkcs
Copy link
Contributor

@ahkcs ahkcs commented Sep 23, 2025

Summary

Value-storing aggregate functions (first, last, min, max) failed with a ClassCastException when targeting deeply nested field paths such as resource.attributes.telemetry.sdk.language.
This PR fixes nested-field value extraction in OpenSearch SQL by handling Map/HashMap objects that represent nested structures, eliminating the unsafe cast and restoring expected behavior for value-storing aggregates.


Problem

These queries previously failed with ClassCastException for value-storing aggregates:

source=logs-otel-v1* | stats first(`resource.attributes.telemetry.sdk.language`) by severityNumber
source=logs-otel-v1* | stats last(`resource.attributes.telemetry.sdk.language`) by severityNumber
source=logs-otel-v1* | stats min(`resource.attributes.telemetry.sdk.name`) by severityNumber
source=logs-otel-v1* | stats max(`resource.attributes.telemetry.sdk.name`) by severityNumber

While simple fields worked:

source=logs-otel-v1* | stats first(severityNumber)

Note: Calculation-only aggregates like count() and dc() were unaffected because they don’t store field values.

Observed error:

{
  "error": {
    "reason": "There was internal problem at backend",
    "details": "class java.util.HashMap cannot be cast to class java.lang.String",
    "type": "ClassCastException"
  }
}

Root Cause

Execution-path analysis showed the failure at the final type-conversion layer in value-storing aggregates:

  1. Field resolution returned partial Map structures (e.g., {attributes={telemetry={sdk={language=java}}}}) instead of the terminal primitive ("java").
  2. Aggregates (first, last, min, max) stored these Map objects in their accumulators.
  3. Type conversion later attempted (String) value in ObjectContent.stringValue() where value was a Map.

Why calculation-only aggregates work: functions like count() and dc() perform calculations without storing the actual field values, so they never hit this conversion path.


Solution

Implement nested-map handling in ObjectContent.stringValue() so value-storing aggregate outputs can be safely converted to strings.

Key changes

  • Detect Map values and recursively extract the terminal primitive via extractFinalPrimitiveValue(...).
  • Preserve existing behavior for non-Map values (backward compatible).

Before

return (String) value;  // throws ClassCastException if value is a Map

After

if (value instanceof Map) {
  Map<String, Object> map = (Map<String, Object>) value;
  Object finalValue = extractFinalPrimitiveValue(map); // drills down single-value maps
  if (finalValue != null && !(finalValue instanceof Map)) {
    return finalValue.toString();
  }
  return map.toString();
}
return (String) value;

Example extraction
Input: {attributes={telemetry={sdk={language=java}}}}
{telemetry={sdk={language=java}}}
{sdk={language=java}}
{language=java}
"java"


Verification (now succeeds)

source=logs-otel-v1* | stats first(`resource.attributes.telemetry.sdk.language`) by severityNumber
source=logs-otel-v1* | stats last(`resource.attributes.telemetry.sdk.language`) by severityNumber
source=logs-otel-v1* | stats min(`resource.attributes.telemetry.sdk.language`) by severityNumber
source=logs-otel-v1* | stats max(`resource.attributes.telemetry.sdk.language`) by severityNumber

Resolves

@ahkcs ahkcs changed the title Fix ClassCastException in first/last aggregates for nested fields Fix ClassCastException for aggregates on deeply nested PPL fields Sep 23, 2025
@ahkcs ahkcs changed the title Fix ClassCastException for aggregates on deeply nested PPL fields Fix ClassCastException for value-storing aggregates on nested PPL fields Sep 23, 2025
Signed-off-by: Kai Huang <[email protected]>

# Conflicts:
#	integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLAggregationIT.java
Signed-off-by: Kai Huang <[email protected]>
Signed-off-by: Kai Huang <[email protected]>
@yuancu yuancu added the bug Something isn't working label Sep 24, 2025
"source=%s | stats min(`resource.attributes.telemetry.sdk.language`) as min_lang,"
+ " max(`resource.attributes.telemetry.sdk.language`) as max_lang",
TEST_INDEX_TELEMETRY));
verifySchema(actual, schema("min_lang", "string"), schema("max_lang", "string"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the type of nested language is an integer, can the ppl work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in ObjectContent.java, first the extractFinalPrimitiveValue method will extract the value as Object and then we'll add toString() to it.

Copy link
Member

@LantaoJin LantaoJin Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I mean what if the mapping contains

                    "properties": {
                      "version": {
                        "type": "integer"
                      }

and query with | stats min(`resource.attributes.telemetry.sdk.version`).
Can you update the mapping and data, and test above query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the approach - moved the fix from ObjectContent.java to OpenSearchExprValueFactory.construct() for better universal handling. Updated the telemetry mapping to test for types like integer and boolean

@ahkcs ahkcs requested a review from LantaoJin September 25, 2025 00:11
Signed-off-by: Kai Huang <[email protected]>
Map<String, Object> map = (Map<String, Object>) value;
if (map.size() == 1) {
Object singleValue = map.values().iterator().next();
return extractFinalPrimitiveValue(singleValue);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there risk of a RecursionError here?

If this is only loaded from an index, it's maybe fine (I think you can't index massively nested structures, not sure). But if there's a path where this is executed on user input, it'd be very easy to cause a crash with this.

Copy link
Member

@LantaoJin LantaoJin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM if the CI rerun passed.

@LantaoJin LantaoJin merged commit e92bf68 into opensearch-project:main Sep 26, 2025
53 of 54 checks passed
@opensearch-trigger-bot
Copy link
Contributor

The backport to 2.19-dev failed:

The process '/usr/bin/git' failed with exit code 128

To backport manually, run these commands in your terminal:

# Navigate to the root of your repository
cd $(git rev-parse --show-toplevel)
# Fetch latest updates from GitHub
git fetch
# Create a new working tree
git worktree add ../.worktrees/sql/backport-2.19-dev 2.19-dev
# Navigate to the new working tree
pushd ../.worktrees/sql/backport-2.19-dev
# Create a new branch
git switch --create backport/backport-4360-to-2.19-dev
# Cherry-pick the merged commit of this pull request and resolve the conflicts
git cherry-pick -x --mainline 1 e92bf68f67577ba42112e47232ffe86f6c32c909
# Push it to GitHub
git push --set-upstream origin backport/backport-4360-to-2.19-dev
# Go back to the original working tree
popd
# Delete the working tree
git worktree remove ../.worktrees/sql/backport-2.19-dev

Then, create a pull request where the base branch is 2.19-dev and the compare/head branch is backport/backport-4360-to-2.19-dev.

@LantaoJin
Copy link
Member

@ahkcs please manually backport it, thanks

ahkcs added a commit to ahkcs/sql that referenced this pull request Sep 26, 2025
@LantaoJin LantaoJin added the backport-manually Filed a PR to backport manually. label Sep 28, 2025
penghuo pushed a commit that referenced this pull request Sep 29, 2025
…ates on nested PPL fields (#4360) (#4395)

* Fix `ClassCastException` for value-storing aggregates on nested PPL fields (#4360)

Signed-off-by: Kai Huang <[email protected]>
(cherry picked from commit e92bf68)

* remove unrelated tests

Signed-off-by: Kai Huang <[email protected]>

---------

Signed-off-by: Kai Huang <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport 2.19-dev backport-failed backport-manually Filed a PR to backport manually. bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] ClassCastException for value-storing aggregates on nested PPL fields

4 participants