Query Information
PPL Command/Query:
source=coll_obj | spath input=body output=log | fields log.level
Expected Result:
log.level should return the value extracted from the JSON in body, i.e. ERROR.
log.level
---------
ERROR
Actual Result:
log.level returns the pre-existing mapped value instead. The spath extraction is silently discarded — no error, no warning.
log.level
------------
MAPPED-DEBUG
Dataset Information
Dataset/Schema Type
Index Mapping
log is an object field whose subfields log.level and log.src are mapped as keyword. body holds a JSON string that also contains a level key.
{
"mappings": {
"properties": {
"log": {
"properties": {
"level": { "type": "keyword" },
"src": { "type": "keyword" }
}
},
"body": { "type": "text" }
}
}
}
Sample Data
{
"log": { "level": "MAPPED-DEBUG", "src": "real-object" },
"body": "{\"level\":\"ERROR\",\"msg\":\"from json\"}"
}
Bug Description
Issue Summary:
When a spath output name collides with an existing object field that has mapped subfields, references to <output>.<key> silently resolve to the pre-existing mapped subfield rather than the extracted JSON value. The extraction is discarded with no error.
This is inconsistent with the flat-keyword collision case, which does not fail silently:
existing mapping of log |
spath input=body output=log | fields log.level |
keyword |
returns the extracted value ERROR; and misusing log as a string raises a clear error: EQUAL function expects {[IP,IP],[COMPARABLE_TYPE,COMPARABLE_TYPE]}, but got [STRUCT,STRING] |
object with log.level: keyword |
returns the stale mapped value MAPPED-DEBUG, silently |
Related but distinct from #5152 (MAP paths failing to resolve in downstream commands) and #5185 (eval dropping the MAP root). In both of those, resolution fails and the user sees an error. Here resolution succeeds and silently answers from the wrong source.
Steps to Reproduce:
- Enable the Calcite engine:
curl -s -X PUT 'localhost:9200/_cluster/settings' -H 'Content-Type: application/json' \
-d '{"transient":{"plugins.calcite.enabled":true}}'
- Create the index with
log as an object having mapped subfields:
curl -s -X PUT 'localhost:9200/coll_obj' -H 'Content-Type: application/json' -d '{
"mappings": {"properties": {
"log": {"properties": {"level": {"type":"keyword"}, "src": {"type":"keyword"}}},
"body": {"type": "text"}
}}}'
- Index the sample document:
curl -s -X POST 'localhost:9200/coll_obj/_doc?refresh=true' -H 'Content-Type: application/json' \
-d '{"log":{"level":"MAPPED-DEBUG","src":"real-object"},
"body":"{\"level\":\"ERROR\",\"msg\":\"from json\"}"}'
- Run the query — the mapped value is returned instead of the extracted one:
source=coll_obj | spath input=body output=log | fields log.level
log.level = MAPPED-DEBUG (expected: ERROR)
- Symptom A — per-subfield split. Within a single row,
log.* draws from two different sources depending on whether each individual leaf happens to be mapped:
source=coll_obj | spath input=body output=log | fields log.level, log.msg, log.src
log.level | log.msg | log.src
MAPPED-DEBUG | from json | real-object
^mapped ^spath ^mapped
log.level is mapped, so the mapped value wins. log.msg is not in the mapping, so the spath value is used. fields log alone does return the full extracted map, confirming the extraction did happen and is simply unreachable through log.level:
source=coll_obj | spath input=body output=log | fields log
{'msg': 'from json', 'level': 'ERROR'}
- Symptom B — filters silently return wrong results. Filtering for the extracted value matches nothing, with no error; filtering for the stale mapped value matches:
source=coll_obj | spath input=body output=log | where log.level = 'ERROR' | fields log.level
source=coll_obj | spath input=body output=log | where log.level = 'MAPPED-DEBUG' | fields log.level
- Symptom C — dynamic mapping silently retires a working extraction.
log.msg resolves to the spath output only because it is currently unmapped. Index one unrelated document containing log.msg so dynamic mapping adds the subfield:
curl -s -X POST 'localhost:9200/coll_obj/_doc?refresh=true' -H 'Content-Type: application/json' \
-d '{"log":{"level":"X","src":"y","msg":"DYNAMICALLY-MAPPED"},
"body":"{\"level\":\"E2\",\"msg\":\"json-2\"}"}'
Re-run the same query. The answer for the original, unmodified document changes from from json to null:
source=coll_obj | spath input=body output=log | fields log.msg
before: from json
after: null (original doc)
DYNAMICALLY-MAPPED (newly indexed doc)
- Related oddity — path mode into a colliding name.
log and log.level end up as unrelated columns, where log.level is not a subfield of log:
source=coll_obj | spath input=body output=log path=level | fields log, log.level
log | log.level
ERROR | MAPPED-DEBUG
Impact:
Queries return stale values from the index instead of freshly extracted JSON, silently. Symptom B is the most damaging: a where clause on the extracted field returns zero rows with no error, so a dashboard or alert filtering on log.level can silently stop matching anything.
Symptom C means this is not stable over time — an extraction that works today can start returning null after an unrelated document triggers dynamic mapping, with no change to the query or to the data being queried.
Because the keyword-collision case raises a clear type error, users can reasonably assume collisions are always reported. In the object case they are not.
Expected behavior
Either behavior would be acceptable, provided it is consistent with the keyword case and never silent:
- Preferred —
spath output=log shadows the entire log.* subtree, so log.level reads the extracted value; or
- reject the query with a clear error when
output collides with an existing object field that has mapped subfields, mirroring the existing [STRUCT,STRING] error in the keyword case.
At minimum, a silent per-leaf mix of mapped and extracted values under one log.* prefix should not be possible.
Workaround
Use a non-colliding output name. Both values are then reachable and mean what was written:
source=coll_obj | spath input=body output=parsed | fields parsed.level, log.level
parsed.level | log.level
ERROR | MAPPED-DEBUG
Environment Information
OpenSearch Version:
Branch 3.8, built from source — plugin 3.8.0.0-SNAPSHOT on OpenSearch 3.8.0-SNAPSHOT. Calcite engine enabled (plugins.calcite.enabled=true).
Additional Details:
main is expected to be affected as well — the spath and field-resolution code paths involved are unchanged between 3.8 and main. This was not verified against a main build.
docs/user/ppl/cmd/spath.md does not currently document collision behavior for the output parameter, which makes the difference between the keyword case (clear error) and the object case (silent) hard to discover.
Screenshots
N/A — all output above is from the _plugins/_ppl REST endpoint.
Query Information
PPL Command/Query:
Expected Result:
log.levelshould return the value extracted from the JSON inbody, i.e.ERROR.Actual Result:
log.levelreturns the pre-existing mapped value instead. Thespathextraction is silently discarded — no error, no warning.Dataset Information
Dataset/Schema Type
Index Mapping
logis anobjectfield whose subfieldslog.levelandlog.srcare mapped askeyword.bodyholds a JSON string that also contains alevelkey.{ "mappings": { "properties": { "log": { "properties": { "level": { "type": "keyword" }, "src": { "type": "keyword" } } }, "body": { "type": "text" } } } }Sample Data
{ "log": { "level": "MAPPED-DEBUG", "src": "real-object" }, "body": "{\"level\":\"ERROR\",\"msg\":\"from json\"}" }Bug Description
Issue Summary:
When a
spathoutputname collides with an existing object field that has mapped subfields, references to<output>.<key>silently resolve to the pre-existing mapped subfield rather than the extracted JSON value. The extraction is discarded with no error.This is inconsistent with the flat-
keywordcollision case, which does not fail silently:logspath input=body output=log | fields log.levelkeywordERROR; and misusinglogas a string raises a clear error:EQUAL function expects {[IP,IP],[COMPARABLE_TYPE,COMPARABLE_TYPE]}, but got [STRUCT,STRING]log.level: keywordMAPPED-DEBUG, silentlyRelated but distinct from #5152 (MAP paths failing to resolve in downstream commands) and #5185 (
evaldropping the MAP root). In both of those, resolution fails and the user sees an error. Here resolution succeeds and silently answers from the wrong source.Steps to Reproduce:
logas an object having mapped subfields:log.*draws from two different sources depending on whether each individual leaf happens to be mapped:log.levelis mapped, so the mapped value wins.log.msgis not in the mapping, so thespathvalue is used.fields logalone does return the full extracted map, confirming the extraction did happen and is simply unreachable throughlog.level:log.msgresolves to thespathoutput only because it is currently unmapped. Index one unrelated document containinglog.msgso dynamic mapping adds the subfield:Re-run the same query. The answer for the original, unmodified document changes from
from jsontonull:logandlog.levelend up as unrelated columns, wherelog.levelis not a subfield oflog:Impact:
Queries return stale values from the index instead of freshly extracted JSON, silently. Symptom B is the most damaging: a
whereclause on the extracted field returns zero rows with no error, so a dashboard or alert filtering onlog.levelcan silently stop matching anything.Symptom C means this is not stable over time — an extraction that works today can start returning
nullafter an unrelated document triggers dynamic mapping, with no change to the query or to the data being queried.Because the keyword-collision case raises a clear type error, users can reasonably assume collisions are always reported. In the object case they are not.
Expected behavior
Either behavior would be acceptable, provided it is consistent with the keyword case and never silent:
spath output=logshadows the entirelog.*subtree, solog.levelreads the extracted value; oroutputcollides with an existing object field that has mapped subfields, mirroring the existing[STRUCT,STRING]error in the keyword case.At minimum, a silent per-leaf mix of mapped and extracted values under one
log.*prefix should not be possible.Workaround
Use a non-colliding
outputname. Both values are then reachable and mean what was written:Environment Information
OpenSearch Version:
Branch
3.8, built from source — plugin3.8.0.0-SNAPSHOTon OpenSearch3.8.0-SNAPSHOT. Calcite engine enabled (plugins.calcite.enabled=true).Additional Details:
mainis expected to be affected as well — thespathand field-resolution code paths involved are unchanged between3.8andmain. This was not verified against amainbuild.docs/user/ppl/cmd/spath.mddoes not currently document collision behavior for theoutputparameter, which makes the difference between the keyword case (clear error) and the object case (silent) hard to discover.Screenshots
N/A — all output above is from the
_plugins/_pplREST endpoint.