opensearch-project / opensearch-project/sql
[BUG] Janino compile failures return the query plan in the error response
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 176
- Forks
- 229
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 43
Description
Query Information
PPL Command/Query:
source=app_logs_object | eval s = concat(`dimensions.pod`, 'x') | fields s
Any query whose generated Java fails to compile reaches this path; passing an object field to a function expecting a string is one easy way to get there.
Expected Result: a generic failure. The generated Java did not compile, which is an engine defect — no part of the plan, the generated source, or the Java type names helps the person who ran the query.
Actual Result: the query plan is returned to the client, twice, and the only human-readable part is buried in a third field:
{
"error": {
"context": {
"stage": "executing",
"plan": "LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(s=[CONCAT($3, 'x')])\n CalciteLogicalIndexScan(table=[[OpenSearch, app_logs_object]])\n",
"stage_description": "Running the query"
},
"reason": "Error while preparing plan [LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(s=[CONCAT($3, 'x')])\n CalciteLogicalIndexScan(table=[[OpenSearch, app_logs_object]])\n]",
"details": "Line 23, Column 33: Assignment conversion not possible from type \"java.util.Map\" to type \"java.lang.String\"",
"location": [
"while running the query",
"while compiling the optimized query plan for physical execution"
],
"code": "PLANNING_ERROR",
"type": "SQLException"
},
"status": 500
}
On a real query the plan is far larger — the report that led to #5750 produced a ~2 KB dump of 20+ RelNode lines as the reason, which is what clients display, making the failure look undiagnosable.
Dataset Information
Dataset/Schema Type
- OpenTelemetry (OTEL)
- Simple Schema for Observability (SS4O)
- Open Cybersecurity Schema Framework (OCSF)
- Custom (details below)
Index Mapping
{"mappings": {"properties": {
"@timestamp": {"type": "date"},
"message": {"type": "text"},
"dimensions": {"properties": {"pod": {"properties": {
"name": {"type": "keyword"}, "uid": {"type": "keyword"}}}}}
}}}
Sample Data
{"@timestamp": "2026-01-01T00:01:00Z", "message": "request served", "dimensions": {"pod": {"name": "pod-a", "uid": "u-1"}}}
Bug Description
Issue Summary
When Janino fails to compile the generated Java, the error response carries the query plan in reason and again in context.plan. It should carry a generic internal-error message; the plan, the generated source and the stack trace belong in the node log only.
Steps to Reproduce
curl -s -XPUT 'localhost:9200/app_logs_object' -H 'Content-Type: application/json' -d '{
"mappings": {"properties": {
"@timestamp": {"type": "date"},
"message": {"type": "text"},
"dimensions": {"properties": {"pod": {"properties": {
"name": {"type": "keyword"}, "uid": {"type": "keyword"}}}}}
}}}'
curl -s -XPOST 'localhost:9200/app_logs_object/_doc?refresh=true' -H 'Content-Type: application/json' \
-d '{"@timestamp": "2026-01-01T00:01:00Z", "message": "request served", "dimensions": {"pod": {"name": "pod-a", "uid": "u-1"}}}'
curl -s -XPUT 'localhost:9200/_cluster/settings' -H 'Content-Type: application/json' \
-d '{"transient":{"plugins.calcite.enabled":true}}'
curl -s -XPOST 'localhost:9200/_plugins/_ppl' -H 'Content-Type: application/json' \
-d '{"query":"source=app_logs_object | eval s = concat(`dimensions.pod`, '"'"'x'"'"') | fields s"}'
Impact
- Internal representation is exposed in a user-facing API for a class of failure the user cannot act on.
- The one useful line sits in
detailswhile clients renderreason, so the actual cause is easy to miss — this is precisely why #5750 was initially hard to triage. - The dump inflates error payloads to kilobytes.
Root Cause
Three independent decisions combine:
- Calcite's exception message is the plan. A planning/codegen failure surfaces as
SQLException("Error while preparing plan [<plan>]"). That is upstream behaviour. ErrorReportinherits that message and cannot override it —super(builder.cause.getMessage(), builder.cause)(common/src/main/java/org/opensearch/sql/common/error/ErrorReport.java:49). There is noreason/messagesetter on the builder.reasonis back-filled from the cause.ErrorReport.toJsonMap()never emitsreason, soErrorMessage.getErrorAsJson(opensearch/src/main/java/org/opensearch/sql/opensearch/response/error/ErrorMessage.java:71-78) falls back tocause.getLocalizedMessage()— the plan again.
Separately, CalciteToolsHelper.enrichErrorsForSpecialCases (core/src/main/java/org/opensearch/sql/calcite/utils/CalciteToolsHelper.java:490-498) deliberately copies the plan into context.plan and puts the root cause into details. So the useful message is already computed — it is just not the field clients show.
For the record, no stack frames are serialized into the response; nothing in the main sources writes a stack trace to a payload. What leaks is the plan, the Java exception class name, and Java type names from the compiler message. The stack trace itself only reaches the node log, which is correct.
Proposed Fix
Draw the line by failure class, and let only the first one describe itself to the user:
| Failure class | Cause | Response |
|---|---|---|
| Unsupported or invalid input | the query | 4xx naming the field/feature and why (e.g. #5751) |
| Janino/codegen failure | the engine generated invalid Java | 500 with a generic message; no plan, no generated source, no Java type names |
Planner limitation (CannotPlanException) |
mixed | 4xx/501 with a feature-level message, no plan |
Concretely:
- Detect the codegen class. The chain is
SQLException("Error while preparing plan [...]")→RuntimeException("Error while compiling generated Java code:")→org.codehaus.commons.compiler.CompileException.enrichErrorsForSpecialCasesalready pattern-matches this shape for two narrower cases (WIDTH_BUCKET,CalciteEnumerableNestedAggregate), so aCompileExceptionbranch fits the existing structure. - Add a
reasonoverride toErrorReport.Builderand set it for that branch, e.g.Internal error while compiling the query plan. See the OpenSearch logs for details. - Stop putting the plan in the response. Remove
context.plan, or gate it behind a debug setting that is off by default.explainis the supported way to obtain a plan. - Keep the log verbose — full stack trace, plan and generated source at ERROR, which is what an on-call engineer needs. This already happens.
Note that a Janino failure reaching a user is always an engine bug, so the goal is for this handler never to fire in practice: each reachable trigger should get an input-level guard like #5751's. The generic message is the safety net, not the plan.
Environment Information
OpenSearch Version: main, with plugins.calcite.enabled: true.
Additional Details
- #5750 / #5751 — the report where a ~2 KB plan dump in
reasonhidCannot cast "java.util.Map" to "java.lang.String". That fix guards one trigger before compilation; this issue is about the handler, which is unchanged. - Same family, worth folding into the same review: an aggregation over an object field returns a 500 whose
reasonembeds a shard-level message including a node address, an index UUID and a transport action name (RemoteTransportException[[node][127.0.0.1:PORT][indices:data/read/search[phase/query]]]; nested: QueryShardException[No mapping found for [dimensions.pod] in order to sort on]). That text comes from OpenSearch rather than from this plugin, but it is the same question of how much internal detail a user-facing error should carry.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with CalciteToolsHelper.enrichErrorsForSpecialCases and trace the error through ErrorReport.java and ErrorMessage.java. Review the existing CompileException handling and the builder’s cause-derived message behavior. Done means Janino failures return a generic 500 response without the plan or Java internals, while detailed diagnostics remain in node logs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100