Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[BUG] A field mapped as different types across indices returns changed or null values with HTTP 200

Aperta
#5,828 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@cnoramut ci sta già lavorando.

Dal 21/10/2026.

  • #5902 di @cnoramut — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
java
Ambito
backend, databases

Direzione di ricerca

Reproduce the mixed_a/mixed_b case with the provided curl commands, then read OpenSearchDescribeIndexRequest.getFieldTypes, MergeRuleHelper, OpenSearchResponse.addParsedHitsToBuilder, and OpenSearchExprValueFactory.parse. Trace how the merged field type is applied to each hit and compare the result with _source. Done means the query no longer silently changes or drops valid values, with behavior matching the agreed mismatch policy.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug PPL

Query Information

PPL Command/Query:

source=mixed_* | fields host, f

f is long in mixed_a and date in mixed_b, which is what a rollover mapping change leaves behind.

Expected Result: HTTP 200 with both values as stored, or HTTP 400 naming f and its type in each index. No numeric or date type holds both values.

Actual Result: HTTP 200, with one of the two values silently wrong. Which one depends on the merged type of f. When describe mixed_* reports f as timestamp, the long document's 5 comes back as 1970-01-01 00:00:00.005. That is 5 read as epoch milliseconds, a unit taken from mixed_b's date mapping, while mixed_a stores it as a plain number.

{
  "schema": [
    {
      "name": "host",
      "type": "string"
    },
    {
      "name": "f",
      "type": "timestamp"
    }
  ],
  "datarows": [
    [
      "a",
      "1970-01-01 00:00:00.005"
    ],
    [
      "b",
      "2026-01-01 00:00:00"
    ]
  ],
  "total": 2,
  "size": 2
}

When it reports bigint, the same query returns ["a", 5] and ["b", null], so the date is gone. A raw _search over mixed_* returns "f": 5 and "f": "2026-01-01T00:00:00Z" in _source, so the stored data is intact.

The same happens for many other pairs, with the wrong value in bold.

f in index A f in index B merged type of f A's document returns B's document returns
keyword "300" byte 1 tinyint 44 1
integer 300 byte 1 tinyint 44 1
long 5000000000 byte 1 tinyint 0 1
long 5000000000 integer 3 int 705032704 3
integer 40000 short 1 smallint -25536 1
keyword "300" date timestamp 1970-01-01 00:00:00.3 2026-01-01 00:00:00
byte 1 date timestamp 1970-01-01 00:00:00.001 2026-01-01 00:00:00
integer 3 date timestamp 1970-01-01 00:00:00.003 2026-01-01 00:00:00
long 5 date timestamp 1970-01-01 00:00:00.005 2026-01-01 00:00:00
byte 1 date tinyint 1 null
integer 3 date int 3 null
long 5 date bigint 5 null
text "hello world" byte 1 tinyint null 1
text "hello world" integer 3 int null 3
text "hello world" long 5 bigint null 5
text "hello world" date timestamp null 2026-01-01 00:00:00
keyword "abc" long 5 bigint null 5
double 1.5 long 5 bigint null 5
float 2.5 integer 3 int null 3

The date value is 2026-01-01T00:00:00Z throughout. The merged type is whichever index the mapping merge reads last, as #5752 explains. On the same node it changed when only the index names changed. Under the other merged type each pair returned both values without loss, as strings when that type is string. The exception is the number and date pairs, which are wrong under both. These pairs lost no value under any merged type seen:

  • text against keyword, which always merges to string
  • keyword "300" against integer, long or double, where the string converts to the number
  • keyword "true" against boolean, and a keyword holding a valid address against ip
  • byte, integer and long against each other while every value fits the narrower type

Dataset Information

Dataset/Schema Type

  • OpenTelemetry (OTEL)
  • Simple Schema for Observability (SS4O)
  • Open Cybersecurity Schema Framework (OCSF)
  • Custom (details below)

Index Mapping

PUT mixed_a { "mappings": { "properties": { "host": { "type": "keyword" }, "f": { "type": "long" } } } }
PUT mixed_b { "mappings": { "properties": { "host": { "type": "keyword" }, "f": { "type": "date" } } } }

Sample Data

POST mixed_a/_doc/1?refresh=true  { "host": "a", "f": 5 }
POST mixed_b/_doc/1?refresh=true  { "host": "b", "f": "2026-01-01T00:00:00Z" }

Each index alone returns its own value correctly.

Bug Description

Issue Summary

PPL gives a field one type for the whole index pattern and decodes every document's _source value as that type, including documents from indices that map the field differently.

OpenSearchDescribeIndexRequest.getFieldTypes merges the per-index mappings through MergeRuleHelper. Only three shapes get a rule of their own. They are text or match_only_text against keyword, text with a keyword subfield against text without one, and two object or two nested fields, which merge their subfields. Every other pair falls through to LatestRule. Its mergeInto is target.put(key, source), so the last index read wins. The method returns only the merged map. Its own comment at line 100 reads // TODO possible collision if two indices have fields with the same name and different mappings.

OpenSearchResponse.addParsedHitsToBuilder then passes each hit's hit.getSourceAsString() to OpenSearchExprValueFactory.construct, whose type map is the merged one. The Calcite engine and the v2 engine share this path. A value from the losing index goes wrong in one of three ways.

  1. Integers narrow without a range check. OpenSearchJsonContent.byteValue at line 50 is return (byte) parseLongValue(value());, and intValue and shortValue at lines 35 and 45 cast the same way. parseLongValue also accepts a numeric string, so "300" becomes the long 300 and then the byte 44. The same cast narrows on a single index too when index.mapping.ignore_malformed is on, where 300 in a byte field returns 44. The #5618 catch in item 2 does not help, since the cast returns a value and never throws.
  2. A value that does not parse becomes null. OpenSearchExprValueFactory.parse wraps the per-type decode in catch (Exception e) { return ExprNullValue.of(); } at lines 237 to 241, with no log line. #5618 added that catch for ignore_malformed documents. "hello world", "abc" or a date string under a number, 1.5 or 2.5 under an integer type, and "hello world" under timestamp all end there.
  3. A number reads as epoch milliseconds under a date. createOpenSearchDateType turns a JSON number into Instant.ofEpochMilli at lines 361 to 372. It multiplies by 1000 first only when the date mapping lists epoch_second. On this node, with "format": "strict_date_optional_time||epoch_second" on mixed_b, the 5 came back as 1970-01-01 00:00:05. A string that matches no mapped format falls back at line 342 to DateFieldMapper.getDefaultDateTimeFormatter(). That formatter is strict_date_optional_time||epoch_millis, so "300" becomes 300 ms after the epoch.

This is a concrete case of the mismatched-data half of #5610, which lists null on mismatch as one candidate policy. Here the value is valid in its own index, so a null hides real data, and the narrowing and epoch cases do not return null at all but invent a value. The node log records no warning or error for any of these requests.

Pushdown off and Calcite off return the same wrong value

With plugins.calcite.pushdown.enabled=false, and separately with plugins.calcite.enabled=false, source=mixed_* | fields host, f returned a body byte-identical to the one under Actual Result. The wrong value does not come from a pushed-down operation. The projection pushes down only as _source filtering, and the decode above runs on both engines.

Later commands inherit the wrong value, and stats narrows its own result

  • A self join on f loses the rows whose key decoded to null. long against date under bigint and text against long under bigint each return 1 of 2 rows. #5825 notes the first case.
  • For long against date under bigint, where isnull(f) returns no rows, because it pushes down as must_not exists and the stored value exists. head 10 | where isnull(f), which filters in memory, returns the row that fields shows as null. The same filter gives two answers depending on where it runs.
  • stats max(f) computes over the indexed values, and ObjectContent then narrows the result to the merged type. long 5000000000 against integer under int returns 2147483647, and integer 40000 against short under smallint returns -25536. long against date under bigint returns 1767225600000, the date's epoch milliseconds, while fields shows that same value as null.

Steps to Reproduce

curl -s -XPUT "localhost:9200/mixed_a" -H 'Content-Type: application/json' \
  -d '{"mappings":{"properties":{"host":{"type":"keyword"},"f":{"type":"long"}}}}'
curl -s -XPUT "localhost:9200/mixed_b" -H 'Content-Type: application/json' \
  -d '{"mappings":{"properties":{"host":{"type":"keyword"},"f":{"type":"date"}}}}'
curl -s -XPOST "localhost:9200/mixed_a/_doc/1?refresh=true" -H 'Content-Type: application/json' -d '{"host":"a","f":5}'
curl -s -XPOST "localhost:9200/mixed_b/_doc/1?refresh=true" -H 'Content-Type: application/json' -d '{"host":"b","f":"2026-01-01T00:00:00Z"}'

# 1. The merged type of f, bigint or timestamp depending on the node
curl -s -XPOST "localhost:9200/_plugins/_ppl" -H 'Content-Type: application/json' \
  -d '{"query":"describe mixed_* | where COLUMN_NAME = '\''f'\'' | fields TYPE_NAME"}'

# 2. Returns HTTP 200 with one of the two values changed or null
curl -s -XPOST "localhost:9200/_plugins/_ppl" -H 'Content-Type: application/json' \
  -d '{"query":"source=mixed_* | fields host, f"}' -w '\nHTTP %{http_code}\n'

# 3. The raw search returns both values as stored
curl -s -XPOST "localhost:9200/mixed_*/_search?filter_path=hits.hits._source" -H 'Content-Type: application/json' -d '{}'

This pair is wrong under either merged type, so it reproduces on any node.

Impact

In an index-pattern deployment, one rolled-over index that maps a field differently makes queries over the whole pattern return wrong values with HTTP 200 and no warning. Counts, filters, joins and charts built on the field are then wrong in ways a user cannot see from the response. Which value is wrong can change between nodes and after a restart, because the merged type does.

Environment Information

OpenSearch Version: main at 67aed0ee8, 3.10.0-SNAPSHOT, one node, with the defaults plugins.calcite.enabled=true, plugins.calcite.pushdown.enabled=true and plugins.calcite.fallback.allowed=false.

Additional Details:

  • #5752 is the same LatestRule merge for object against a scalar, and explains why the merged type differs between nodes. It covers only the object pairs, so every pair above is outside it.
Lingua principale
Java
Stelle
175
Fork
232
Merge medio
3g 3h
PR unite (30g)
42

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di opensearch-project/sql

Tutte le issue di opensearch-project/sql

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.