Skip to content

[BUG Fix] Report an expand of something that is not a column, rather than casting it - #5841

Open
RyanL1997 wants to merge 2 commits into
opensearch-project:mainfrom
RyanL1997:fix/expand-non-column-field
Open

RyanL1997 wants to merge 2 commits into
opensearch-project:mainfrom
RyanL1997:fix/expand-non-column-field

Conversation

@RyanL1997

Copy link
Copy Markdown
Collaborator

Description

expand correlates the expansion with a column of its input, and the resolved field was cast to a RexInputRef unchecked:

RexInputRef arrayFieldRex = (RexInputRef) rexVisitor.analyze(arrayField, context);

A leaf of an object or a map reaches the planner as an ITEM call, and an expression as its own call, so the cast failed with

java.lang.ClassCastException: class org.apache.calcite.rex.RexCall cannot be cast to
class org.apache.calcite.rex.RexInputRef

and the user was shown Calcite class names instead of anything about their query.

It is now reported the way the mvexpand check a few lines below already reports its own unsupported target: the field by name, what expand takes, and what to do instead.

Cannot expand [ATTRS.k]
  expand takes a field of the index, not a value computed from one.
  Expand the field itself, or project the value first.

Testing

CalcitePPLExpandTest.testExpandOfAValueThatIsNotAColumn — a table with a map column, asserting that expand ATTRS.k does not surface as a ClassCastException and that the message names the field. It fails on main and passes here. :core:test, :ppl:test and spotlessCheck pass.

Issues Resolved

Closes #5840

Split out of #5830, which reached the cast with a flat_object leaf. The cast predates that change and is reached without it.

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • Commits are signed per the DCO using --signoff.

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.

…ng it

expand correlates the expansion with a column of its input, and the resolved
field was cast to a RexInputRef unchecked. A leaf of an object or a map reaches
the planner as an ITEM call, and an expression as its own call, so the cast
failed as a ClassCastException and the user was shown Calcite class names
instead of anything about their query.

Report it the way the mvexpand check a few lines below already does: the field
by name, what expand takes, and what to do instead.

Closes opensearch-project#5840

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 55e0b03)

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

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 55e0b03
Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle potential null field value

The error message construction should handle potential null values from
arrayField.getField(). If getField() returns null, calling toString() will throw a
NullPointerException before the intended error can be reported. Add a null check or
use a safe default value.

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java [4465-4476]

-String name = StringUtils.unquoteIdentifier(arrayField.getField().toString());
+String fieldStr = arrayField.getField() != null ? arrayField.getField().toString() : "unknown";
+String name = StringUtils.unquoteIdentifier(fieldStr);
 throw ErrorReport.wrap(
         new IllegalArgumentException(StringUtils.format("Cannot expand [%s]", name)))
     .code(ErrorCode.UNSUPPORTED_OPERATION)
     .stage(QueryProcessingStage.ANALYZING)
     .location("while resolving the field for expand")
     .context("command", "expand")
     .context("field", name)
     .suggestion(
         "expand takes a field of the index, not a value computed from one. Expand the field"
             + " itself, or project the value first.")
     .build();
Suggestion importance[1-10]: 5

__

Why: While adding null safety is generally good practice, the suggestion assumes arrayField.getField() could return null without evidence from the PR context. The code already successfully calls arrayField.getField().toString() later at line 4480, suggesting null is not expected. This is a defensive programming improvement rather than fixing an actual bug.

Low

Previous suggestions

Suggestions up to commit 2a495b4
CategorySuggestion                                                                                                                                    Impact
General
Fix inconsistent indentation in error construction

The error message construction uses inconsistent indentation for the
ErrorReport.wrap() call. The opening parenthesis should align with subsequent method
calls for better readability and maintainability.

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java [4466-4476]

 throw ErrorReport.wrap(
-        new IllegalArgumentException(StringUtils.format("Cannot expand [%s]", name)))
-    .code(ErrorCode.UNSUPPORTED_OPERATION)
-    .stage(QueryProcessingStage.ANALYZING)
-    .location("while resolving the field for expand")
-    .context("command", "expand")
-    .context("field", name)
-    .suggestion(
-        "expand takes a field of the index, not a value computed from one. Expand the field"
-            + " itself, or project the value first.")
-    .build();
+            new IllegalArgumentException(StringUtils.format("Cannot expand [%s]", name)))
+        .code(ErrorCode.UNSUPPORTED_OPERATION)
+        .stage(QueryProcessingStage.ANALYZING)
+        .location("while resolving the field for expand")
+        .context("command", "expand")
+        .context("field", name)
+        .suggestion(
+            "expand takes a field of the index, not a value computed from one. Expand the field"
+                + " itself, or project the value first.")
+        .build();
Suggestion importance[1-10]: 3

__

Why: This is a minor code style improvement addressing indentation consistency. While it improves readability, it has minimal impact on functionality or correctness. The existing code is already functional and the indentation difference is subtle.

Low

@RyanL1997 RyanL1997 changed the title [BUG] Report an expand of something that is not a column, rather than casting it [BUG Fix] Report an expand of something that is not a column, rather than casting it Oct 6, 2026
@RyanL1997 RyanL1997 mentioned this pull request Oct 6, 2026
3 tasks done
RyanL1997 added a commit to RyanL1997/sql that referenced this pull request Oct 6, 2026
A flat_object field was dropped by OpenSearchDataType.parseMapping as an
unknown type, so it was absent from the schema and any reference to it failed
as "field not found" (opensearch-project#1604, opensearch-project#3067).

It is now a struct keyed by the dotted path of each leaf. The field declares
no sub-fields, so a leaf has no type of its own: every value reads as text,
and a path holding several values reads as their JSON. That is the rule spath
already applies to a JSON string, so there is one rule to learn rather than
two, and typeof on a leaf reports string like any other text column.

Both spellings reach the same leaf -- {"a":{"b":1}} and {"a.b":1} each give
the entry a.b -- and a document writing both keeps both values, in the order
it wrote them.

A leaf is text, so PPL does with it what it does with any text column: read
it, cast it (cast(attributes.duration_ms as double) gives 12.5 for "12.5" and
null for "n/a"), filter, sort, group. Nothing is restricted that is not
already restricted for a spath field.

The one exception is expand, which needs a column of the index to correlate the
expansion with; a leaf is not one. It is reported there as a ClassCastException,
which opensearch-project#5841 fixes for every field rather than only for a leaf.

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
RyanL1997 added a commit to RyanL1997/sql that referenced this pull request Oct 6, 2026
Both gaps a leaf filter can fall into are existing ones -- the index holds a
value the engine does not read back identically, which a plain keyword or
integer field reaches too -- and the paragraph said so only in its second
clause. It says so first now, and adds what a reader wants next: a leaf meets
fewer of them than a mapped field, since without doc values it never reaches
the index for sort, dedup, stats ... by, top or rare.

The expand note read as a continuation of the multi-value bullet above it. It
is its own limitation and is set out as one.

The expand guard itself moves to opensearch-project#5841, which fixes it for every field rather
than only for a leaf, so its case goes with it.

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.76%. Comparing base (803ed31) to head (55e0b03).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
.../opensearch/sql/calcite/CalciteRelNodeVisitor.java 0.00% 12 Missing ⚠️

❌ Your project check has failed because the head coverage (63.76%) 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    #5841      +/-   ##
============================================
+ Coverage     63.36%   63.76%   +0.40%     
- Complexity     8872     9063     +191     
============================================
  Files           949      962      +13     
  Lines         40322    40985     +663     
  Branches       4546     4654     +108     
============================================
+ Hits          25549    26136     +587     
- Misses        13929    13973      +44     
- Partials        844      876      +32     
Flag Coverage Δ
sql-engine 63.76% <0.00%> (+0.40%) ⬆️

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.

RyanL1997 added a commit to RyanL1997/sql that referenced this pull request Oct 6, 2026
Both gaps a leaf filter can fall into belong to the engine, not to this field
type: the index holds a value the engine does not read back identically, which
a plain keyword or integer field reaches the same way. opensearch-project#5834 records them.
Stating them here reads as though a flat_object introduced them.

expand goes with them. It takes a column of the index to correlate the
expansion with, which is not about a leaf either -- any expand target that is
not a column of its input reaches the same place, and opensearch-project#5841 reports it for all
of them.

What is left is the field type: how a leaf reads, what the index answers, and
what costs a record per candidate and why.

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
The unit test pins the planner. What the issue is about is the response: a
query that asked for something expand cannot do came back as HTTP 500 naming
Calcite classes.

spath's auto-extract mode gives a map column, and a leaf of one is reached with
ITEM, so `spath input=body | expand body.tags` is the shape a user reaches this
through. Without the fix the case fails with

  500, "class org.apache.calcite.rex.RexCall cannot be cast to class
       org.apache.calcite.rex.RexInputRef"

and with it the body carries the field, the code and what to do instead. A
field of the index is expanded as before, asserted alongside.

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 55e0b03

Comment on lines +65 to +67
- match: {"$body": "/Cannot\\s+expand\\s+.body.tags./"}
- match: {"$body": "/UNSUPPORTED_OPERATION/"}
- match: {"$body": "/expand\\s+takes\\s+a\\s+field\\s+of\\s+the\\s+index/"}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we make this assertion more readable?

Field arrayField = expand.getField();
RexInputRef arrayFieldRex = (RexInputRef) rexVisitor.analyze(arrayField, context);
RexNode resolved = rexVisitor.analyze(arrayField, context);
if (!(resolved instanceof RexInputRef arrayFieldRex)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is a negation so the name should not be arrayFieldRex?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] expand on a field that is not a column of the input fails as a ClassCastException

2 participants