Skip to content

feat(core): aggregation values and window statements read by type (SCHEMA-2b) - #454

Merged
fupelaqu merged 3 commits into
mainfrom
feature/SCHEMA-2b
Nov 1, 2026
Merged

fupelaqu merged 3 commits into
mainfrom
feature/SCHEMA-2b

Conversation

@fupelaqu

@fupelaqu fupelaqu commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Slice 2 of 2 of the schema-aware result path in core (SCHEMA-2b, after #453). Every value core returns from an aggregation response (bucket keys, metrics, top_hits values, the global metrics beside hits) or from a window-enriched statement now has its declared column's class, read where it is first read. Both sides of a window's partition join read each partition column by one rule.

Closes no issue: #418's remaining part (_source: false for projections of computed columns only) and #421's JDBC and Arrow sides are later work.

What changes

  • Aggregation readers (AggregateReading, internal): one reader per bucket key, metric and top_hits column, built once per statement and carried with the statement's schema. A value a reader cannot read exactly stays as 0.24.0 read it (a fractional, NaN or out-of-range metric is kept, never truncated), and the empty-input rule runs first (ES 8's 0.0 over an empty group stays NULL).

  • SUM width: SUM over a whole-number type is declared BIGINT, SUM over REAL / DOUBLE DOUBLE, as DuckDB widens a sum. Every consumer of the declared type follows: describe, set-operation positions, arithmetic over a SUM, CTAS / materialized-view columns.

  • Window statements: base rows are read by type, the rows of a statement without * hold only the SELECT's columns, and one function per side builds the partition key. A whole-day DATE key is read exactly. A date partition column whose format core reads as stored text (a format with a zone name) is matched by its instant: the base request asks Elasticsearch for the column's epoch_millis doc value and pairs it with the bucket's millisecond key, selected or not; the displayed value stays the stored text.

  • Docs: core pages that feat(core): declared ResultSchema and hit values read by type (SCHEMA-2) #453 and this PR had made stale now describe the measured behaviour: GROUP BY key classes, aggregate types (in a UNION ALL, in type conversion, SUM over no row), set-operation dates, window partitions, row projection, and the JDBC / Arrow column types.

Release notes

  • Classes on aggregation and window routes: BOOLEAN keys Boolean (was the text "true"); date keys java.time at Z (were text or millis); BIGINT keys Long, INT keys Integer; MIN / MAX over integers Integer / Long (was Double); SUM over integers Long (was Double); COUNT Long (ES 8 answered Double); REAL Float; top_hits values (FIRST_VALUE, LAST_VALUE, ARRAY_AGG elements) read by their column's type; AggregateApi boolean MIN / MAX answer BooleanValue. Values are unchanged.
  • SUM over integers is BIGINT: describe and set-operation positions report it; a new CTAS or materialized-view column built from it maps long (existing indexes are untouched). A UNION ALL of such SUMs beyond 2^31 answered a wrapped Integer on 0.24.0 (1705032704 for 6,000,000,000); it answers the Long. Painless over a SUM moves a cast (((long) p.sum) + 1 becomes p.sum + 1), with the same values.
  • Windows: BOOLEAN, TIMESTAMP and whole-day DATE partitions are answered, and so is a BIGINT partition holding epoch-millisecond-sized values (they answered NULL); a date partition read as stored text is matched by its instant and answered, selected or not, unless another window's partition column is not returned (NULL on 0.24.0); a window SUM over integers answers a Long; the rows of a window statement without * hold only the SELECT's columns.
  • Materialized-view quota (softclient4es-extensions): the extension counts the existing materialized views with SELECT count(*) and reads the count only as Int / Long / String. On ES 8 that count was a Double, read as 0, so the licence's cap on the number of materialized views was never reached there. It is now a Long: on ES 8, a CREATE MATERIALIZED VIEW beyond the cap is refused, as it already was on ES 6.

Acceptance evidence

Check Evidence
describe equals the result's schema, no search issued unit (counting double = 0 searches); SchemaReadingSpec on ES 8.18.3
One class per column on every aggregation and window route SchemaReadingSpec on ES 8.18.3 and 6.8.23 (one-shot, async, scroll, _msearch legs); canned pages per client form. Counted exclusions: FIRST / LAST on aggregation-only and GROUP BY rows (a map, as on 0.24.0), inexact DATE keys, stats / percentile maps, ARRAY_AGG's column-level type
Expected answers for the analysis's aggregation rows SchemaReadingSpec on ES 8.18.3 and 6.8.23
Window oracle SchemaReadingSpec on ES 8.18.3 and 6.8.23 over one-shot, async and scroll; the zone-name partition on ES 8 (6.8 cannot parse zone names)
No statement answered on 0.24.0 is refused Client legs on the branch, ES 8.18.3 and 6.8.23 (area specs incl. window, GROUP BY, aggregate, UNION ALL, subquery, date, set-operation, gateway, REPL). A probe of 31 statements (window shapes incl. index patterns and aliases with mixed date / keyword mappings, date_nanos, multi-valued partitions; SUM / MIN / MAX / COUNT, Painless over a SUM, empty groups, CTAS) on 0.24.0 and on the branch, ES 8.18.3, 322 lines: 90 differ, each a fix or a class change listed above; none refused
Downstream Throwaway local publish, deleted afterwards. softclient4es-arrow: SELECT k, SUM(n) … GROUP BY k over an integer field, three values of 2,000,000,000, streams s: Int(64) = 6000000000 (Long). softclient4es-jdbc: getObject Long 6000000000 / 3, getLong the same. softclient4es-extensions: a materialized view over SUM(f.n) AS t, SUM(f.n) + 1 AS s answers Long 6000000000 / 6000000001, columns BIGINT, s mapped long. Each compiles with #453's consumer-shape edits only

Cost (warm JIT, setup split from the row loop, branch runs alternated with main runs on the same machine; medians of 5 pairs):

Measure main this PR Bound
Reader build per statement 13.9 / 12.7 / 12.6 µs 12.8 / 14.0 / 14.3 µs ≤ 25 µs
Per aggregation bucket-row 13,006 / 12,529 / 13,177 ns 10,301 / 9,506 / 9,564 ns ≤ main × 1.03
Per window base row 4,044 / 3,363 / 3,593 ns 3,923 / 3,794 / 3,510 ns ≤ main × 1.03

Three runs: a quiet one, then two at a machine load of 12 to 16. The window-row bound holds in the first and third runs; in the second (load 12) the branch was 1.13 to 1.24 × main on three of four warm pairs. The probe measures an internal overload that rebuilds the partition keys per row, which the real path builds once per statement, so its window figure is pessimistic.

Recorded, not fixed

Measured the same on 0.24.0 and on this branch:

  • A SELECT item named like a PARTITION BY column (CONCAT(pz, '') AS pz) is never computed: the column and the window values read NULL.
  • A row whose partition misses nulls the base column named like the window's operand (n of SUM(n) OVER … AS s).
  • A partition column the SELECT does not return (and no *) makes every window of the statement read NULL, including another window whose partition would be answered alone, and nulls the window's operand column.
  • OVER () beside hits: the aliases read NULL and the values come back under internal keys (sum#s, value_count#c, max#m).
  • A document without the partition column reads NULL, not the NULL partition's aggregate; a date_nanos partition, a partition projected under another name, and a pattern or alias mixing date and keyword mappings for the column read NULL.
  • SELECT *, <expr> rows carry a * key holding NULL (window and plain statements).
  • CTAS of a GROUP BY populates one all-NULL row per document instead of one row per group.
  • HAVING SUM(n) > 3000000000 fails in Painless ("Invalid int constant"); a scalar subquery value beyond the int range compared with an integer column is refused by Elasticsearch.
  • WHERE subqueries over a GROUP BY: k IN (SELECT k … GROUP BY k) over a keyword holding "10:30" fails (read as a time), big IN (SELECT big … GROUP BY big) over a long is refused by Elasticsearch; inner statements keep 0.24.0's reading in this slice.
  • CAST(SUM(n) AS INT) is refused at parse.

Test plan

  • core/test (1,426 passed), sql/test (2,170 passed; one refusal text re-pinned from INT to BIGINT for SUM, then 16 / 16), sbt "+ compile" (2.12 + 2.13)
  • Docker-free client specs and the three bridge test suites (es6 rest, es6 jest, es7 rest, es8 java; bridges es6 / es7 / es8), all green
  • ES 8.18.3: area leg, then the window and schema specs after each fix round
  • ES 6.8.23: area leg, then the window and schema specs after the fix rounds
  • CI lint line (headerCheck scalafmtSbtCheck scalafmtCheck test:scalafmtCheck) on the committed bytes
  • ES 7.17 and ES 9 legs, es6 jest leg (CI)

🤖 Generated with Claude Code

fupelaqu and others added 3 commits October 31, 2026 17:01
…HEMA-2b)

Story SCHEMA-2b. Reviewed: r0:must-fix, r1:must-fix, r2:clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions (SCHEMA-2b)

GROUP BY key classes, aggregate types (UNION ALL, type conversion, SUM over no
row), set-operation dates, window partitions, row projection, and the JDBC /
Arrow column types, rewritten to what SCHEMA-2 and SCHEMA-2b measure.

Story SCHEMA-2b. Reviewed: docs r0:must-fix, r1:must-fix, r2:must-fix (one clause, fixed as prescribed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The no-matching-document paragraph still said SUM over no value is 0.0 on
every column; it now follows the aggregate page: 0 over an integer column,
0.0 over a REAL or DOUBLE one.

Story SCHEMA-2b. Reviewed: docs r3:clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant