Skip to content

Commit 8394e5c

Browse files
authored
fix(sql): Report invalid-query errors as client errors (#5532)
UnifiedQueryPlanner.plan() let field-not-found (ErrorReport) and Calcite validation errors such as table-not-found (CalciteException) fall through to the catch-all and rethrew them as IllegalStateException. Propagate ErrorReport unwrapped, map CalciteException to SemanticCheckException, and log client errors at WARN. Also unwrap ErrorReport to its cause in the SQL error formatter so the reported type reflects the cause, not the wrapper. Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent 8cce7a8 commit 8394e5c

4 files changed

Lines changed: 52 additions & 4 deletions

File tree

api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.apache.calcite.rel.RelRoot;
1515
import org.apache.calcite.rel.core.Sort;
1616
import org.apache.calcite.rel.logical.LogicalSort;
17+
import org.apache.calcite.runtime.CalciteException;
1718
import org.apache.calcite.sql.SqlKind;
1819
import org.apache.calcite.sql.SqlNode;
1920
import org.apache.calcite.sql.util.SqlVisitor;
@@ -25,6 +26,7 @@
2526
import org.opensearch.sql.ast.tree.UnresolvedPlan;
2627
import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
2728
import org.opensearch.sql.common.antlr.SyntaxCheckException;
29+
import org.opensearch.sql.common.error.ErrorReport;
2830
import org.opensearch.sql.exception.QueryEngineException;
2931
import org.opensearch.sql.exception.SemanticCheckException;
3032

@@ -74,13 +76,18 @@ public RelNode plan(String query) {
7476
} catch (SyntaxCheckException
7577
| QueryEngineException
7678
| UnsupportedOperationException
77-
| IllegalArgumentException e) {
78-
LOG.error("Failed to plan query: {}", e.getMessage());
79+
| IllegalArgumentException
80+
| ErrorReport e) {
81+
LOG.warn("Failed to plan query: {}", e.getMessage());
7982
throw e;
83+
} catch (CalciteException e) {
84+
// Calcite validation errors (e.g. table not found) indicate an invalid query.
85+
LOG.warn("Failed to plan query, invalid query: {}", e.getMessage());
86+
throw new SemanticCheckException(e.getMessage(), e);
8087
} catch (AssertionError e) {
8188
// Calcite throws assertion error directly when building bad RelNode
8289
String message = "Failed to plan query: invalid plan structure";
83-
LOG.error(message, e);
90+
LOG.warn(message, e);
8491
throw new SemanticCheckException(message, e);
8592
} catch (Exception e) {
8693
String message = "Failed to plan query: unexpected error";

api/src/test/java/org/opensearch/sql/api/UnifiedQueryPlannerTest.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
import java.util.Map;
1212
import org.apache.calcite.rel.RelNode;
13+
import org.apache.calcite.runtime.CalciteException;
1314
import org.apache.calcite.schema.Schema;
1415
import org.apache.calcite.schema.impl.AbstractSchema;
1516
import org.junit.Test;
1617
import org.opensearch.sql.common.antlr.SyntaxCheckException;
18+
import org.opensearch.sql.common.error.ErrorReport;
1719
import org.opensearch.sql.exception.SemanticCheckException;
1820
import org.opensearch.sql.executor.QueryType;
1921

@@ -72,7 +74,7 @@ public void testPPLQueryPlanningWithDefaultNamespaceMultiLevel() {
7274

7375
// This is valid in SparkSQL, but Calcite requires "catalog" as the default root schema to
7476
// resolve it
75-
assertThrows(IllegalStateException.class, () -> planner.plan("source = opensearch.employees"));
77+
assertThrows(SemanticCheckException.class, () -> planner.plan("source = opensearch.employees"));
7678
}
7779

7880
@Test
@@ -131,6 +133,20 @@ public void semanticErrorIsRethrownAsSemanticCheckException() {
131133
.assertErrorMessageEquals("Source and target patterns have different wildcard counts");
132134
}
133135

136+
@Test
137+
public void fieldNotFoundIsRethrownAsErrorReport() {
138+
givenInvalidQuery("source = catalog.employees | where unknown_field = 1")
139+
.assertErrorType(ErrorReport.class)
140+
.assertErrorMessageContains("Field [unknown_field] not found");
141+
}
142+
143+
@Test
144+
public void invalidTableIsRethrownAsSemanticCheckException() {
145+
givenInvalidQuery("source = catalog.nonexistent_table")
146+
.assertErrorType(SemanticCheckException.class)
147+
.assertCauseType(CalciteException.class);
148+
}
149+
134150
@Test
135151
public void assertionErrorIsWrappedAsSemanticCheckException() {
136152
// Remove when the underlying Calcite assertion is fixed.

legacy/src/main/java/org/opensearch/sql/legacy/executor/format/ErrorMessageFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.opensearch.sql.legacy.executor.format;
77

88
import org.opensearch.OpenSearchException;
9+
import org.opensearch.sql.common.error.ErrorReport;
910

1011
public class ErrorMessageFactory {
1112
/**
@@ -25,6 +26,10 @@ public static ErrorMessage createErrorMessage(Exception e, int status) {
2526
OpenSearchException exception = (OpenSearchException) unwrapCause(e);
2627
return new OpenSearchErrorMessage(exception, exception.status().getStatus());
2728
}
29+
// Unwrap ErrorReport so the error type reflects the underlying cause, not the wrapper.
30+
if (e instanceof ErrorReport) {
31+
return new ErrorMessage(((ErrorReport) e).getCause(), status);
32+
}
2833
return new ErrorMessage(e, status);
2934
}
3035

legacy/src/test/java/org/opensearch/sql/legacy/unittest/ErrorMessageFactoryTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.junit.Test;
1010
import org.opensearch.OpenSearchException;
1111
import org.opensearch.core.rest.RestStatus;
12+
import org.opensearch.sql.common.error.ErrorReport;
1213
import org.opensearch.sql.legacy.executor.format.ErrorMessage;
1314
import org.opensearch.sql.legacy.executor.format.ErrorMessageFactory;
1415
import org.opensearch.sql.legacy.executor.format.OpenSearchErrorMessage;
@@ -42,6 +43,25 @@ public void nonOpenSearchExceptionWithWrappedEsExceptionCauseShouldCreateEsError
4243
Assert.assertTrue(msg instanceof OpenSearchErrorMessage);
4344
}
4445

46+
@Test
47+
public void errorReportShouldRenderUnderlyingCauseType() {
48+
Exception exception =
49+
ErrorReport.wrap(new IllegalArgumentException("Field [x] not found.")).build();
50+
ErrorMessage msg =
51+
ErrorMessageFactory.createErrorMessage(exception, RestStatus.BAD_REQUEST.getStatus());
52+
Assert.assertFalse(msg instanceof OpenSearchErrorMessage);
53+
Assert.assertTrue(msg.toString().contains("IllegalArgumentException"));
54+
Assert.assertFalse(msg.toString().contains("ErrorReport"));
55+
}
56+
57+
@Test
58+
public void errorReportWrappingEsExceptionShouldCreateEsErrorMessage() {
59+
Exception exception = ErrorReport.wrap(new OpenSearchException(nonOpenSearchThrowable)).build();
60+
ErrorMessage msg =
61+
ErrorMessageFactory.createErrorMessage(exception, RestStatus.NOT_FOUND.getStatus());
62+
Assert.assertTrue(msg instanceof OpenSearchErrorMessage);
63+
}
64+
4565
@Test
4666
public void
4767
nonOpenSearchExceptionWithMultiLayerWrappedEsExceptionCauseShouldCreateEsErrorMessage() {

0 commit comments

Comments
 (0)