Skip to content

Commit 8d8103f

Browse files
committed
updates
Signed-off-by: Kai Huang <ahkcs@amazon.com>
1 parent bca5a72 commit 8d8103f

2 files changed

Lines changed: 57 additions & 18 deletions

File tree

integ-test/src/yamlRestTest/resources/rest-api-spec/test/issues/4896.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ teardown:
7676
- match: { "datarows.0.0": "2025-11-26 17:10:00" }
7777
- match: { "datarows.0.1": "test message" }
7878

79-
# Query all fields including log - should succeed without crash
80-
# The log field with invalid "." field name will have null for that field
79+
# Query the log field specifically - should succeed without crash
80+
# The log field with invalid "." field name will have null for that invalid subfield
8181
- do:
8282
headers:
8383
Content-Type: 'application/json'
8484
ppl:
8585
body:
86-
query: source=test_disabled_object_4896
86+
query: source=test_disabled_object_4896 | fields log
8787
- match: { "total": 1 }
88+
# The log field returns an empty object because the invalid "." field resolves to null
89+
# and null values are omitted from JSON serialization
90+
- match: { "datarows.0.0": {} }

opensearch/src/main/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactory.java

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@
3232
import java.time.format.DateTimeParseException;
3333
import java.time.temporal.TemporalAccessor;
3434
import java.util.ArrayList;
35+
import java.util.HashSet;
3536
import java.util.List;
3637
import java.util.Map;
3738
import java.util.Optional;
39+
import java.util.Set;
3840
import java.util.function.BiFunction;
3941
import lombok.Getter;
4042
import lombok.Setter;
@@ -76,6 +78,13 @@
7678
public class OpenSearchExprValueFactory {
7779
private static final Logger LOG = LogManager.getLogger(OpenSearchExprValueFactory.class);
7880

81+
/**
82+
* Collects invalid field names encountered during parsing. Uses ThreadLocal to avoid log spam by
83+
* logging all invalid fields once per construct() call instead of per field.
84+
*/
85+
private static final ThreadLocal<Set<String>> INVALID_FIELDS =
86+
ThreadLocal.withInitial(HashSet::new);
87+
7988
/** The Mapping of Field and ExprType. */
8089
private final Map<String, OpenSearchDataType> typeMapping;
8190

@@ -168,14 +177,31 @@ public OpenSearchExprValueFactory(
168177
* </pre>
169178
*/
170179
public ExprValue construct(String jsonString, boolean supportArrays) {
180+
INVALID_FIELDS.get().clear();
171181
try {
172-
return parse(
173-
new OpenSearchJsonContent(OBJECT_MAPPER.readTree(jsonString)),
174-
TOP_PATH,
175-
Optional.of(STRUCT),
176-
fieldTypeTolerance || supportArrays);
182+
ExprValue result =
183+
parse(
184+
new OpenSearchJsonContent(OBJECT_MAPPER.readTree(jsonString)),
185+
TOP_PATH,
186+
Optional.of(STRUCT),
187+
fieldTypeTolerance || supportArrays);
188+
logInvalidFields();
189+
return result;
177190
} catch (JsonProcessingException e) {
178191
throw new IllegalStateException(String.format("invalid json: %s.", jsonString), e);
192+
} finally {
193+
INVALID_FIELDS.get().clear();
194+
}
195+
}
196+
197+
/** Log all invalid fields encountered during parsing (once per construct call). */
198+
private void logInvalidFields() {
199+
Set<String> invalidFields = INVALID_FIELDS.get();
200+
if (!invalidFields.isEmpty()) {
201+
LOG.warn(
202+
"The following field(s) have invalid names and will return null: {}. "
203+
+ "This can happen with disabled object fields that bypass field name validation.",
204+
invalidFields);
179205
}
180206
}
181207

@@ -380,25 +406,35 @@ private ExprValue parseStruct(Content content, String prefix, boolean supportArr
380406
entry -> {
381407
String fieldKey = entry.getKey();
382408
String fullFieldPath = makeField(prefix, fieldKey);
383-
try {
409+
// Check for invalid field names (e.g., fields consisting only of dots like "." or
410+
// "..")
411+
// before creating JsonPath to avoid masking other IllegalArgumentExceptions
412+
if (isInvalidFieldName(fieldKey)) {
413+
// Collect invalid fields to log once per construct() call to avoid log spam
414+
INVALID_FIELDS.get().add(fullFieldPath);
415+
result.tupleValue().put(fieldKey, ExprNullValue.of());
416+
} else {
384417
populateValueRecursive(
385418
result,
386419
new JsonPath(fieldKey),
387420
parse(entry.getValue(), fullFieldPath, type(fullFieldPath), supportArrays));
388-
} catch (IllegalArgumentException e) {
389-
// Return null for invalid field names (e.g., fields consisting only of dots)
390-
// This can happen with disabled object fields that bypass field name validation
391-
// Log warning to hint users about corrupt data
392-
LOG.warn(
393-
"Field '{}' has invalid name and will return null: {}",
394-
fullFieldPath,
395-
e.getMessage());
396-
result.tupleValue().put(fieldKey, ExprNullValue.of());
397421
}
398422
});
399423
return result;
400424
}
401425

426+
/**
427+
* Check if a field name is invalid. A field name is invalid if it consists only of dots (e.g.,
428+
* ".", "..", "..."). Such field names cause issues because String.split("\\.") returns an empty
429+
* array for them.
430+
*
431+
* @param fieldName The field name to check.
432+
* @return true if the field name is invalid, false otherwise.
433+
*/
434+
private static boolean isInvalidFieldName(String fieldName) {
435+
return fieldName.split("\\.").length == 0;
436+
}
437+
402438
/**
403439
* Populate the current ExprTupleValue recursively.
404440
*

0 commit comments

Comments
 (0)