Skip to content

Commit 9295c92

Browse files
committed
Adopt ErrorReport for unresolvable alias path error
Wrap the SemanticCheckException in an ErrorReport (the report-builder interface from opensearch-project#5266) so the error carries structured context as it bubbles up: FIELD_NOT_FOUND code, ANALYZING stage, a location chain, the alias field and path as context, and a fix suggestion. On the PPL/Calcite path this renders as a rich structured error; on the SQL JDBC path it still returns a clear 400 (RestSqlAction unwraps to the SemanticCheckException cause), though the JdbcResponseFormatter does not yet render the ErrorReport structure. Update the unit test to assert the ErrorReport code/stage/context/cause, the SQL IT for the ErrorReport type, and add a PPL IT asserting the structured FIELD_NOT_FOUND error in CalciteErrorReportStageIT. Signed-off-by: Jialiang Liang <jiallian@amazon.com>
1 parent 4d1c8e3 commit 9295c92

4 files changed

Lines changed: 82 additions & 25 deletions

File tree

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteErrorReportStageIT.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.io.IOException;
1212
import org.json.JSONObject;
1313
import org.junit.jupiter.api.Test;
14+
import org.opensearch.client.Request;
1415
import org.opensearch.client.ResponseException;
1516
import org.opensearch.sql.ppl.PPLIntegTestCase;
1617

@@ -214,4 +215,36 @@ public void testStageDescriptionIsUserFriendly() throws IOException {
214215
|| stageDescription.toLowerCase().contains("run")
215216
|| stageDescription.toLowerCase().contains("query"));
216217
}
218+
219+
// An alias field whose path targets a text multi-field (e.g. "source.keyword") is not present in
220+
// the flattened mapping. It used to surface an opaque NullPointerException; it must now report a
221+
// structured FIELD_NOT_FOUND error with a suggestion.
222+
@Test
223+
public void testAliasToUnresolvablePathIncludesStructuredError() throws IOException {
224+
String index = "test_alias_unresolved_keyword";
225+
Request createIndex = new Request("PUT", "/" + index);
226+
createIndex.setJsonEntity(
227+
"{ \"mappings\": { \"properties\": {"
228+
+ " \"source\": { \"type\": \"text\", \"fields\": { \"keyword\": { \"type\":"
229+
+ " \"keyword\" } } },"
230+
+ " \"source_alias\": { \"type\": \"alias\", \"path\": \"source.keyword\" } } } }");
231+
client().performRequest(createIndex);
232+
233+
ResponseException exception =
234+
assertThrows(ResponseException.class, () -> executeQuery("source=" + index));
235+
236+
JSONObject error =
237+
new JSONObject(getResponseBody(exception.getResponse())).getJSONObject("error");
238+
239+
assertEquals("FIELD_NOT_FOUND", error.getString("code"));
240+
assertTrue(
241+
"Details should name the alias field and path",
242+
error
243+
.getString("details")
244+
.contains("Alias field [source_alias] refers to unresolved path [source.keyword]"));
245+
JSONObject context = error.getJSONObject("context");
246+
assertEquals("source_alias", context.getString("alias_field"));
247+
assertEquals("source.keyword", context.getString("alias_path"));
248+
assertTrue("Should include a suggestion", error.has("suggestion"));
249+
}
217250
}

integ-test/src/test/java/org/opensearch/sql/sql/QueryValidationIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public void testAliasToKeywordMultiFieldFailsWithBadRequest() throws IOException
117117

118118
expectResponseException()
119119
.hasStatusCode(BAD_REQUEST)
120-
.hasErrorType("SemanticCheckException")
120+
.hasErrorType("ErrorReport")
121121
.containsMessage("Alias field [source_alias] refers to unresolved path [source.keyword]")
122122
.whenExecute(String.format(Locale.ROOT, "SELECT * FROM %s", index));
123123
}

opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataType.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
import lombok.EqualsAndHashCode;
1515
import lombok.Getter;
1616
import org.apache.commons.lang3.EnumUtils;
17+
import org.opensearch.sql.common.error.ErrorCode;
18+
import org.opensearch.sql.common.error.ErrorReport;
19+
import org.opensearch.sql.common.error.QueryProcessingStage;
1720
import org.opensearch.sql.data.type.ExprCoreType;
1821
import org.opensearch.sql.data.type.ExprType;
1922
import org.opensearch.sql.exception.SemanticCheckException;
@@ -300,13 +303,22 @@ private static void validateAliasType(Map<String, OpenSearchDataType> result) {
300303
String originalPath = value.getOriginalPath().get();
301304
OpenSearchDataType target = result.get(originalPath);
302305
if (target == null) {
303-
throw new SemanticCheckException(
304-
String.format(
305-
"Alias field [%s] refers to unresolved path [%s]. The alias path must point"
306-
+ " to an existing field in the mapping; a text multi-field (e.g."
307-
+ " \"%s.keyword\") or a removed/renamed field is not a valid alias"
308-
+ " target.",
309-
key, originalPath, originalPath));
306+
throw ErrorReport.wrap(
307+
new SemanticCheckException(
308+
String.format(
309+
"Alias field [%s] refers to unresolved path [%s].",
310+
key, originalPath)))
311+
.code(ErrorCode.FIELD_NOT_FOUND)
312+
.stage(QueryProcessingStage.ANALYZING)
313+
.location("while resolving alias fields in the index mapping")
314+
.context("alias_field", key)
315+
.context("alias_path", originalPath)
316+
.suggestion(
317+
"The alias path must point to an existing field in the mapping; a text"
318+
+ " multi-field (e.g. \""
319+
+ originalPath
320+
+ ".keyword\") or a removed/renamed field is not a valid alias target.")
321+
.build();
310322
}
311323
result.put(key, new OpenSearchAliasType(originalPath, target));
312324
}

opensearch/src/test/java/org/opensearch/sql/opensearch/data/type/OpenSearchDataTypeTest.java

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
import org.junit.jupiter.params.provider.Arguments;
4242
import org.junit.jupiter.params.provider.EnumSource;
4343
import org.junit.jupiter.params.provider.MethodSource;
44+
import org.opensearch.sql.common.error.ErrorCode;
45+
import org.opensearch.sql.common.error.ErrorReport;
46+
import org.opensearch.sql.common.error.QueryProcessingStage;
4447
import org.opensearch.sql.data.type.ExprCoreType;
4548
import org.opensearch.sql.data.type.ExprType;
4649
import org.opensearch.sql.exception.SemanticCheckException;
@@ -493,16 +496,23 @@ public void traverseAndFlatten_alias_to_unresolvable_path_throws_descriptive_err
493496
textKeywordType,
494497
"source_alias",
495498
new OpenSearchAliasType("source.keyword", OpenSearchDataType.of(MappingType.Invalid)));
496-
SemanticCheckException keywordException =
499+
ErrorReport keywordError =
497500
assertThrows(
498-
SemanticCheckException.class,
499-
() -> OpenSearchDataType.traverseAndFlatten(keywordAliasTree));
500-
assertEquals(
501-
"Alias field [source_alias] refers to unresolved path [source.keyword]. The alias path must"
502-
+ " point to an existing field in the mapping; a text multi-field (e.g."
503-
+ " \"source.keyword.keyword\") or a removed/renamed field is not a valid alias"
504-
+ " target.",
505-
keywordException.getMessage());
501+
ErrorReport.class, () -> OpenSearchDataType.traverseAndFlatten(keywordAliasTree));
502+
assertAll(
503+
() -> assertEquals(ErrorCode.FIELD_NOT_FOUND, keywordError.getCode()),
504+
() -> assertEquals(QueryProcessingStage.ANALYZING, keywordError.getStage()),
505+
() -> assertTrue(keywordError.getCause() instanceof SemanticCheckException),
506+
() ->
507+
assertEquals(
508+
"Alias field [source_alias] refers to unresolved path [source.keyword].",
509+
keywordError.getCause().getMessage()),
510+
() -> assertEquals("source_alias", keywordError.getContext().get("alias_field")),
511+
() -> assertEquals("source.keyword", keywordError.getContext().get("alias_path")),
512+
() ->
513+
assertTrue(
514+
keywordError.getSuggestion().contains("\"source.keyword.keyword\"")
515+
&& keywordError.getSuggestion().contains("not a valid alias target")));
506516

507517
// Alias path targets a field that does not exist.
508518
Map<String, OpenSearchDataType> missingFieldTree =
@@ -511,15 +521,17 @@ public void traverseAndFlatten_alias_to_unresolvable_path_throws_descriptive_err
511521
textType,
512522
"col_alias",
513523
new OpenSearchAliasType("missing", OpenSearchDataType.of(MappingType.Invalid)));
514-
SemanticCheckException missingException =
524+
ErrorReport missingError =
515525
assertThrows(
516-
SemanticCheckException.class,
517-
() -> OpenSearchDataType.traverseAndFlatten(missingFieldTree));
518-
assertEquals(
519-
"Alias field [col_alias] refers to unresolved path [missing]. The alias path must point to"
520-
+ " an existing field in the mapping; a text multi-field (e.g. \"missing.keyword\") or"
521-
+ " a removed/renamed field is not a valid alias target.",
522-
missingException.getMessage());
526+
ErrorReport.class, () -> OpenSearchDataType.traverseAndFlatten(missingFieldTree));
527+
assertAll(
528+
() -> assertEquals(ErrorCode.FIELD_NOT_FOUND, missingError.getCode()),
529+
() -> assertTrue(missingError.getCause() instanceof SemanticCheckException),
530+
() ->
531+
assertEquals(
532+
"Alias field [col_alias] refers to unresolved path [missing].",
533+
missingError.getCause().getMessage()),
534+
() -> assertEquals("missing", missingError.getContext().get("alias_path")));
523535
}
524536

525537
@Test

0 commit comments

Comments
 (0)