Skip to content

Commit e533a78

Browse files
committed
Fix opaque NullPointerException for unresolvable alias-type field path
When a mapping contains a field of "type": "alias" whose "path" points to a target absent from the flattened mapping (a text multi-field such as field.keyword, or a removed/renamed field), validateAliasType passed a null target into the OpenSearchAliasType constructor, which dereferenced it at super(type.getExprCoreType()) and surfaced an opaque NullPointerException. Guard the null target and throw a SemanticCheckException naming the alias field and its unresolved path. SemanticCheckException extends QueryEngineException, so JdbcResponseFormatter maps it to HTTP 400 (client error) rather than the misleading 500 a generic exception would produce. Add unit tests covering the .keyword multi-field and missing-field cases. Fixes opensearch-project#5535 Signed-off-by: Jialiang Liang <jiallian@amazon.com>
1 parent 804d4f1 commit e533a78

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.apache.commons.lang3.EnumUtils;
1717
import org.opensearch.sql.data.type.ExprCoreType;
1818
import org.opensearch.sql.data.type.ExprType;
19+
import org.opensearch.sql.exception.SemanticCheckException;
1920

2021
/** The extension of ExprType in OpenSearch. */
2122
@EqualsAndHashCode
@@ -297,7 +298,17 @@ private static void validateAliasType(Map<String, OpenSearchDataType> result) {
297298
(key, value) -> {
298299
if (value instanceof OpenSearchAliasType && value.getOriginalPath().isPresent()) {
299300
String originalPath = value.getOriginalPath().get();
300-
result.put(key, new OpenSearchAliasType(originalPath, result.get(originalPath)));
301+
OpenSearchDataType target = result.get(originalPath);
302+
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));
310+
}
311+
result.put(key, new OpenSearchAliasType(originalPath, target));
301312
}
302313
});
303314
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.junit.jupiter.params.provider.MethodSource;
4444
import org.opensearch.sql.data.type.ExprCoreType;
4545
import org.opensearch.sql.data.type.ExprType;
46+
import org.opensearch.sql.exception.SemanticCheckException;
4647

4748
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
4849
class OpenSearchDataTypeTest {
@@ -483,6 +484,43 @@ public void test_AliasType() {
483484
() -> assertEquals("original_path2", aliasTypeOnDouble.getOriginalPath().orElseThrow()));
484485
}
485486

487+
@Test
488+
public void traverseAndFlatten_alias_to_unresolvable_path_throws_descriptive_error() {
489+
// An alias whose path targets a text multi-field (e.g. "source.keyword"). Multi-fields are
490+
// stored under OpenSearchTextType.fields, not properties, so they are never added to the
491+
// flattened mapping and the alias target resolves to null. Previously this surfaced as an
492+
// opaque NullPointerException (issue #5535).
493+
Map<String, OpenSearchDataType> keywordAliasTree =
494+
Map.of(
495+
"source", textKeywordType,
496+
"source_alias",
497+
new OpenSearchAliasType("source.keyword", OpenSearchDataType.of(MappingType.Invalid)));
498+
SemanticCheckException keywordException =
499+
assertThrows(
500+
SemanticCheckException.class,
501+
() -> OpenSearchDataType.traverseAndFlatten(keywordAliasTree));
502+
assertEquals(
503+
"Alias field [source_alias] refers to unresolved path [source.keyword]. The alias path"
504+
+ " must point to an existing field in the mapping; a text multi-field (e.g."
505+
+ " \"source.keyword.keyword\") or a removed/renamed field is not a valid alias target.",
506+
keywordException.getMessage());
507+
508+
// An alias whose path targets a field that does not exist (e.g. renamed/removed).
509+
Map<String, OpenSearchDataType> missingFieldTree =
510+
Map.of(
511+
"col1", textType,
512+
"col_alias", new OpenSearchAliasType("missing", OpenSearchDataType.of(MappingType.Invalid)));
513+
SemanticCheckException missingException =
514+
assertThrows(
515+
SemanticCheckException.class,
516+
() -> OpenSearchDataType.traverseAndFlatten(missingFieldTree));
517+
assertEquals(
518+
"Alias field [col_alias] refers to unresolved path [missing]. The alias path must point to"
519+
+ " an existing field in the mapping; a text multi-field (e.g. \"missing.keyword\") or a"
520+
+ " removed/renamed field is not a valid alias target.",
521+
missingException.getMessage());
522+
}
523+
486524
@Test
487525
public void test_parseMapping_on_AliasType() {
488526
Map<String, Object> indexMapping1 =

0 commit comments

Comments
 (0)