Repository navigation
Wrong results with no error: ES 8 answers 0.0 for MIN / MAX / AVG of a column a whole group lacks, CREATE TABLE … AS SELECT … GROUP BY stores one empty document per source document, and values come back in the class Elasticsearch's JSON gives them (a date per group as epoch milliseconds) #414
Description
Three defects return HTTP 200 with wrong data: item 1 on Elasticsearch 8 only, items 2 and 4 on every build. Item 3 is a loud failure of the same CREATE TABLE … AS SELECT path.
Measured on main (f63222d) and on #412's head (0687815), through the gateway. PostgreSQL 16.14 ran on the same rows.
Fixture
CREATE TABLE t (id KEYWORD, g KEYWORD, n INT, x DOUBLE, d DATE, d2 DATE, …, PRIMARY KEY (id)). "—" means the field is absent from the document; PostgreSQL stores it as NULL.
| id | g | n | x | d | d2 |
|---|---|---|---|---|---|
| r1 | g1 | 3 | 0.25 | 2024-01-31 | 2024-02-01 |
| r2 | g1 | 5 | 1.5 | 2024-03-01 | 2024-02-28 |
| r3 | g2 | -2 | — | 2023-12-25 | — |
| r4 | g3 | — | 2.0 | — | 2024-01-31 |
| r5 | g3 | 10 | -1.25 | 2024-02-29 | 2024-02-29 |
Group g2 has one document, and neither x nor d2 appears in it.
1. ES 8 reports 0.0 for MIN / MAX / AVG over a group whose documents all lack the column
| statement | ES 8.18.3 (main and #412) |
ES 7.17.29 and 6.8.23 (main and #412) |
PostgreSQL 16 |
|---|---|---|---|
SELECT g, MIN(x) AS mn, MAX(x) AS mx, AVG(x) AS av FROM t GROUP BY g, group g2 |
0.0, 0.0, 0.0 | NULL, NULL, NULL | NULL, NULL, NULL |
SELECT g, MIN(d2) AS mn, MAX(d2) AS mx FROM t GROUP BY g, group g2 |
0.0, 0.0 (epoch: 1970-01-01) | NULL, NULL | NULL, NULL |
SELECT MIN(x) AS mn, MAX(x) AS mx, AVG(x) AS av FROM t WHERE g = 'g2' |
0.0, 0.0, 0.0 | NULL, NULL, NULL | NULL, NULL, NULL |
SELECT MIN(x) AS mn, MAX(x) AS mx, AVG(x) AS av FROM t WHERE g = 'zz' (no document) |
NULL, NULL, NULL | NULL, NULL, NULL | NULL, NULL, NULL |
Groups g1 and g3 are right on every major (0.25 / 1.5 / 0.875 and -1.25 / 2.0 / 0.375). COUNT(x) is 0 for g2, as it should be. SUM(x) is 0.0 on every major, which #336 kept deliberately.
This is the residual of #336. That fix recovers NULL when the input has zero documents: docCount.contains(0L) && … nullOverEmptyInput (core/…/client/ElasticConversion.scala:1129–1130 on #412's head). Group g2 has one document, so the ES 8 module's typed response, where Elasticsearch's {"value": null} has already become 0.0, goes through unchanged. ES 7.17 and 6.8 read the JSON, where the value is null.
The wrong value then flows into everything that reads it. A 0.0 minimum compares as a number, and MIN(d2) reads as 1970-01-01.
- Expected: NULL, on every major.
- Fix direction: for
MIN/MAX/AVG, take NULL from the field's own value count (for example a hiddenvalue_countbeside the metric), or read the rawvalueon ES 8. The bucket's document count is not enough.
2. CREATE TABLE … AS SELECT … GROUP BY stores one empty document per source document
| statement | main = #412 (ES 8.18.3) |
PostgreSQL 16 |
|---|---|---|
CREATE TABLE t2 AS SELECT g, MAX(n) AS m FROM t GROUP BY g |
DmlResult(inserted = 5) |
SELECT 3 |
SELECT COUNT(*) FROM t2 |
5 | 3 |
SELECT * FROM t2 |
{}, {}, {}, {}, {}: no g, no m |
(g1, 5), (g2, -2), (g3, 10) |
DESCRIBE TABLE t2 |
g KEYWORD, m INT |
g text, m integer |
the SELECT alone |
g1 5, g2 -2, g3 10 | same |
The table and its declared columns are right, and the SELECT alone is right. What is stored is one document per source document, each without either column. The population goes through INSERT … AS SELECT (GatewayApi.scala:1207–1212), which reads its source with scroll(single) (IndicesApi.scala:1638).
- Expected: one document per group, holding
gandm. - Fix direction: populate a grouped
SELECTfrom its aggregation results (the rows theSELECTreturns), not from a scroll of the source documents. Otherwise refuse it by name until then.
3. CREATE TABLE … AS SELECT of a DATEDIFF fails at index creation
| statement | main = #412 (ES 8.18.3) |
PostgreSQL 16 (d - d2) |
|---|---|---|
CREATE TABLE t3 AS SELECT id, DATEDIFF(d, d2) AS x FROM t |
400 mapper_parsing_exception: Failed to parse mapping: Field [scaling_factor] is required, and no table is created |
creates x integer: -1, 2, NULL, NULL, 0 |
SELECT id, DATEDIFF(d, d2) AS x FROM t |
-1, 2, NULL, NULL, 0 | same |
DATEDIFF reports NUMERIC, and SQLTypeUtils.elasticType maps NUMERIC to scaled_float without a scaling_factor (sql/…/type/SQLTypeUtils.scala:72 on #412, :71 on main). Any other expression reported NUMERIC reaches the same mapping (not executed here).
- Expected: the table is created with an integer column.
- Fix direction: report
DATEDIFF/DATE_DIFF/TIMESTAMPDIFFasBIGINT, and giveNUMERICa mapping Elasticsearch accepts (double, orscaled_floatwith a factor).
4. A value comes back in the class Elasticsearch's JSON gives it: a date per group as epoch milliseconds, a BIGINT as an Integer
| statement | main |
#412 | PostgreSQL 16 |
|---|---|---|---|
SELECT g, MIN(d2) AS mn, MAX(d2) AS mx FROM t GROUP BY g |
g1 1.7067456E12, 1.7090784E12; g3 1.7066592E12, 1.7091648E12 (Doubles) |
same | g1 2024-02-01, 2024-02-28; g3 2024-01-31, 2024-02-29 |
SELECT g, MAX(d) + 1 AS v FROM t GROUP BY g |
fails in Elasticsearch on every major (ES 8: runtime dynamic method [java.lang.Double, toInstant/0] not found) |
1.7093376E12, 1.7035488E12, 1.7092512E12 (Doubles) |
2024-03-02, 2023-12-26, 2024-03-01 |
The numbers are the right instants (1.7093376E12 ms is 2024-03-02), on Elasticsearch 8.18.3, 7.17.29 and 6.8.23 alike. A row SELECT d FROM t answers dates, and a client that reads the column as a DATE gets a Double. #412 makes per-group date arithmetic run (MAX(d) + 1 is a DATE, as PostgreSQL's date + integer), so its result inherits the same representation. Group g2 is item 1.
The same mechanism gives a BIGINT that fits 32 bits back as an Integer: SELECT id, CAST(n AS BIGINT) AS v FROM t answers Integers 3, 5, -2, NULL, 10 on #412 (Elasticsearch 8.18.3), where a client reading a BIGINT column expects Longs (PostgreSQL: bigint).
- Expected: a date (a timestamp for a
TIMESTAMPcolumn) and aLongfor aBIGINT: the type the column or the calculation is reported as. - Fix direction: when a row is built, convert each value to its reported type: a metric or a per-group calculation reported as
DATE/TIMESTAMPfrom epoch milliseconds (Elasticsearch'svalue_as_stringcoversMIN/MAX, but not abucket_script), and a whole number reported asBIGINTto aLong.
Evidence level
RUN through the gateway (items 1 and 4 on ES 8.18.3, 7.17.29 and 6.8.23; items 2 and 3 on ES 8.18.3), on both builds. PostgreSQL 16.14 answers measured. Code locations come from reading #412's head.
Activity
fupelaqu commentedon Oct 6, 2026
A per-group
GREATEST/LEASTanswers NULL when one of its aggregates is empty; DuckDB and PostgreSQL skip the NULLAnother wrong result with no error, in the family of #414 (item 1 has the same fixture shape: a column a whole group lacks).
Measured on core
0.24.0-SNAPSHOT(coremain7fe1d66, #412 merged), through the gateway, on Elasticsearch 8.18.3 and 7.17.29. DuckDB 1.5.5 and PostgreSQL 16.14 ran on the same rows.Fixture
CREATE TABLE zi_gl (id INT, station_id INT, dt DATE, dt2 DATE, n INT, n2 INT, PRIMARY KEY (id)). "—" means the field is absent from the document; DuckDB and PostgreSQL store it as NULL.Station 1 has no
dt2and non2.Measured
SELECT station_id, MAX(dt) AS a, MAX(dt2) AS b, GREATEST(MAX(dt), MAX(dt2)) AS g, LEAST(MAX(dt), MAX(dt2)) AS l FROM zi_gl GROUP BY station_idg,l: NULL, NULLSELECT station_id, MAX(n) AS a, MAX(n2) AS b, GREATEST(MAX(n), MAX(n2)) AS g, LEAST(MAX(n), MAX(n2)) AS l FROM zi_gl GROUP BY station_idSELECT id, GREATEST(dt, dt2) AS g, LEAST(dt, dt2) AS l, GREATEST(n, n2) AS gn, LEAST(n, n2) AS ln FROM zi_glThe row-level form is right; only the per-group form loses the value. On ES 8,
b(the emptyMAX) reads0.0for station 1 (#414 item 1) whilegreads NULL. On ES 7,bis NULL.A materialized view gives the same answer. With SOFTNETWORK-APP/softclient4es-extensions#72, this view stores no
gand nolforone_null, and 7 / 3 (or 2024-02-05 / 2024-02-03 for the date form) forboth, on Elasticsearch 8.18.3 and 7.17.29:(
zi_gl_st: 1one_null, 2both, 3none.) Extensionsmaindoes not show the case: its views store no per-group calculation at all.Cause
Core sends the per-group call as a
bucket_scriptwith no NULL handling and nogap_policy. From the request it sent on Elasticsearch 8.18.3:"g": {"bucket_script": {"buckets_path": {"a": "a", "b": "b"}, "script": "Math.max(params.a, params.b)"}};"script": "Math.max(Math.floor(params.a / 86400000.0) * 86400000.0, Math.floor(params.b / 86400000.0) * 86400000.0)".Elasticsearch's default gap policy,
skip, skips the script for a bucket where one of itsbuckets_pathvalues is missing. So the cell is absent, and reads NULL. The row-level form follows the ruleGREATEST/LEASTwere built with: a NULL argument is skipped, and the result is NULL only when every argument is NULL.Evidence level
The SOFTNETWORK-APP/softclient4es-extensions#72 runs were taken before its final revision, which does not touch these paths. RUN through the gateway on Elasticsearch 8.18.3 and 7.17.29 (core's search, and the view with and without SOFTNETWORK-APP/softclient4es-extensions#72). DuckDB 1.5.5 and PostgreSQL 16.14 answers measured. The
bucket_scriptbodies are the requests core logged.fupelaqu commentedon Oct 7, 2026
In arrow's set operations
Item 4 (a per-group date comes back as epoch milliseconds) also breaks arrow's set operations.
SELECT MIN(d) AS k FROM t UNION SELECT ts AS k FROM tfails in arrow withConversion Error: Unimplemented type for cast (DOUBLE -> TIMESTAMP_MS), because theMIN(d)leg reaches DuckDB as aDOUBLE. DuckDB 1.5.5 answersTIMESTAMPover the same data.Measured through softclient4es-arrow
main(264eac3) with core0.24.0-SNAPSHOT(#412), on Elasticsearch 8.18.3.