fix(bridge,client,core): STDDEV/VARIANCE over a transformed expression — per-major emission (#222) - #290
Merged
Merged
Conversation
…hBodySerializer seam, per-major serializers (#222) Work in progress on story BIDC-3; tests, docs and the final message follow in a later commit. Story BIDC-3 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…efusalBoundarySpec (#222) Story BIDC-3 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y so a refusal is answered at run (#222) Story BIDC-3 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… verbatim; null-safety has one owner (#222) Story BIDC-3 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Story BIDC-3 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…red, elastic4s fallback handler kept, prose + headers (R3-1..R3-9) Independent review of 398dd5e: APPROVE-WITH-FIXES (0 HIGH, 3 MEDIUM, 6 LOW). All applied. R3-2 (rebase): rebased onto origin/feature/BIDC-2 10f3f8f (two further BIDC-2 commits, both touching BOTH AggregationNamingSpec copies). The merged tree now compiles and runs: bridge and es6 bridge 176/176 on 2.12.20 and 2.13.16. A textually clean merge-tree is not a build. R3-8 (route contract): only SYNCHRONOUS throws were converted, so an extension deferring the translation into its own Future still escaped while the PR body, spec and findings log all claimed "answered at run on EVERY route". Implemented rather than scoped: `dispatch(statement).recover { case e: ElasticError => ... }` beside the existing catch, sharing one `refused` helper, pinned by a deferring-extension case in GatewayRefusalBoundarySpec (6/6). A boundary that only catches is half a boundary when the callers include third-party code — the SPI is exactly that. R3-1: the one-case PartialFunction REPLACED elastic4s's `defaultCustomAggregationHandler`, turning a NotImplementedError naming the class into a bare MatchError for anything the library cannot build; now `scriptedExtendedStats orElse defaultCustomAggregationHandler`, with a dummy-Aggregation test in both client modules (es8 + es9 9/9). That test earned itself immediately: the first form declared the composed val ABOVE the val it reads, so the object initialiser saw null and every emission died with ExceptionInInitializerError. Ordering fixed. R3-3: the upstream backports are OPEN and were unnamed — elastic4s#4105 (series/7.x, the one that LIFTS the es7 refusal) and elastic4s#4106 (series/8.x, the one that retires the es8/es9 handler), both cherry-picks of #4100. Retargeted in the watch note and the PR body, no closing keyword. R3-4 / R3-5 / R3-6: prose corrected — the discriminator's scaladoc named a reader that does not read it; the fifth door's justification was wrong (ElasticMultiSearchRequest has ZERO repo-wide references; the verdict and in-code comment were right); TestSerializers.RawDefault claimed a NotImplementedError it never reaches (it returns an asserted sentinel). R3-7: the windowed-scroll fallback arm's new `sql` is kept (it matches the success arm) and is now a release-note item, since it changes that arm's core SQL log line. R3-9: licence headers on the three new bridge/core test files. AD-S3-1' is LEAD-RATIFIED (2026-09-06); AD-S3-1's named mechanism is recorded in the spec as REFUTED BY MEASUREMENT — AggregationBuilderFn matches its typed ExtendedStatsAggregation arm before consulting any custom handler, so a handler keyed on that type is unreachable dead code. Story BIDC-3 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #222
Story BIDC-3 (BI defect-closure epic). PR base =
feature/BIDC-2(STACKED, lead decision 2026-09-06) — BIDC-2's PR is held, not merged, and this story builds on its correctedbuckets_path/ naming (ElasticAggregation.scalaneighbourhood). Cut fromorigin/feature/BIDC-24368d9dc, rebased onto BIDC-2's head10f3f8fc(its review follow-upsa5f376f2·b09aaf27·b5a86cf9·a363f579·10f3f8fc— every rebase clean; the last two touch BOTHAggregationNamingSpeccopies, so the merged tree was re-compiled and re-run, not just merged).git log origin/main..feature/BIDC-3therefore lists BIDC-2's two commits as well — expected; GitHub retargets tomainwhen BIDC-2 merges. Version line0.23.0-SNAPSHOTverified, untouched. NopublishLocal, no GitHub issue operation. Upstream reference: Philippus/elastic4s#4100 (no closing keyword — a WATCH item, see below).Per-major behaviour (lead ruling R-1b)
"extended_stats":{"field":"createdAt"}), orextended_stats: {}rejected by ESextended_statscarries itsscriptSearchBodyBuilderFnwith a handler for the bridge's marker400, "STDDEV/VARIANCE over a transformed expression is not supported on Elasticsearch 7: … (elastic4s#4100) …"es7/rest; lifted when the 7.17 backport ships andelastic74sbumps400, naming Elasticsearch 6 — permanentes6/restandes6/jestRaw-field
STDDEV(salary)is byte-identical on every major (pinned). Every other scripted metric (MAX(YEAR(x)), …) is untouched (pinned: the es8/es9 serializer is byte-identical to the default for any request without a marker, over 7 shapes incl. HAVING/bucket_script/nested).Why the handler cannot key on
ExtendedStatsAggregation— AD-S3-1′ (LEAD-RATIFIED 2026-09-06)The spec's AD-S3-1 named a handler keyed on
ExtendedStatsAggregation: that mechanism is refuted by measurement, not merely superseded — it is unreachable dead code, independently re-derived from the elastic4s 8.18.2 / 9.0.0 sources by the fresh-context reviewer and ratified by the lead. The seam's SHAPE (client-module-injected serializer; template contributes only a discriminator; es6/es7 reject in the client modules) is unchanged; only the handler's key moves, to a marker type.elastic4s's
AggregationBuilderFn(8.18.2, byte-identical in 9.0.0) matches the typed armcase agg: ExtendedStatsAggregation => ExtendedStatsAggregationBuilder(agg, handler)BEFOREcase other => customAggregationHandler(other)— thecustomAggregationseam is consulted only for a type the library does not know. So the bridge binds a transform-bearingextended_stats(both binds: plain:199and windowed:225, es6:200/:226) to a template-owned markerScriptedExtendedStatsAggregation(inner: ExtendedStatsAggregation)— an elastic4sAggregationwhose abstract members are byte-identical on 6.7.8 / 7.17.4 / 8.18.2 / 9.0.0 (javap) — and the es8/es9 handler keys on the marker. The template stays major-agnostic; the seam is aSearchBodySerializertrait injected as an implicit-with-default onrequestToElasticSearchRequestANDsqlQueryToAggregations(both doors), carried onElasticSearchRequest(defaulted last field).SearchBodySerializer.Default(= today's 1-arg builder) REFUSES a marker loudly and by name (it can never emit the script; without the guard elastic4s throwsNotImplementedError("…Please add a PR!")on it — loud on every line, but misleading).Surfacing:
singleSearchToJsonQueryreturns aString, and translation runs synchronously insidesearchAsync/scrollbefore any Future exists — a refusal used to escapeGatewayApi.runas a raw exception (the same route the bridge'srequire(...)failures take). ONE boundary inGatewayApi.run(statement)—catchfor a synchronous throw (every in-tree route) and.recoverondispatchfor a refusal an out-of-tree extension defers into its own Future, so the contract holds for the SPI's callers too (pinned by a deferring-extension test) — the point every route converges on, extension (CoreDqlExtension's quota-capped scroll callsclient.scrolldirectly and never enters an executor) or executor — turns a thrown status-bearingElasticErrorintoElasticFailure(operation = "dql"). The windowed row query's scroll translated its window-aggregation request INSIDEFuture(...)and built its source lazily, so the refusal only failed the stream at materialisation: its translation is now hoisted onto the calling thread (additiveSearchApi.windowAggregationQuery; theprotectedone-argumentexecuteWindowAggregationskeeps its signature). Contract: a refusal known at translation time is answered atrunon every route (GatewayRefusalBoundarySpec, 5 routes).T2 — reproduction captured on the base commit BEFORE the fix (both bridge copies, both doors, both binds)
After, ES 8/9 (door 1; door 2 identical modulo the wrapper):
After, ES 6/7:
ElasticFailure(400, "STDDEV/VARIANCE over a transformed expression is not supported on Elasticsearch 7: the underlying elastic4s builder drops the aggregation script (elastic4s#4100), so the statistic would silently be computed over the raw field. Aggregate over a raw field, or use Elasticsearch 8+.")— before any JSON exists.Null-safety (lead directive 2026-09-06)
The scripts reaching ES 8/9 through
extended_statsfor the first time are the metric scripts the other aggregates already emit (def param1 = (doc[..].size() == 0 ? null : <chain>); param1,(param1 == null) ? null : Double.valueOf(Math.abs(param1))).AggregationNamingSpec's structural guard (both bridge copies) now walks 9 transform-bearingextended_statsshapes (plain + windowed) off the marker tree; the es8/es9 spec re-checks the rendered JSON.Fifth door found, exempt with a comment
ElasticMultiSearchRequest.query(MultiSearchBuilderFn, default handler) is referenced nowhere in the repository — core builds_msearchbodies from each request's ownsingleSearchToJsonQuery(ElasticQueries.multiQuery), andrequestToMultiSearchRequestbuilds elastic4s'sMultiSearchRequest, not this case class. Exempt with a comment; a marker inside it would still fail loudly on every line (elastic4s'sNotImplementedError).queryToJsonexempt (query-only) as specified.Downstream fixture sweep (AC 8, read-only)
extended_stats/STDDEV/VARIANCE/VAR_POP/VAR_SAMP/std_deviation/ElasticSearchRequest/SearchBodyBuilderFn/singleSearchToJsonQuery/elastic.sql.bridgeacrosssoftclient4es-jdbc,softclient4es-arrow,softclient4es-extensions: 0 hits — nothing downstream pins the moved emission or consumes the bridge directly. Binary-incompatible arity onElasticSearchRequest(new defaulted last field) — downstream rebuilds on0.23.0anyway (same class as BIDC-2 / 21.1).Upstream watch (AC 9, AD-S3-3)
Local record
docs/issues/local-BIDC-3-elastic4s-4100-upstream-watch.md(never staged). Three upstream PRs, all OPEN — awaiting merge + release (checked 2026-09-06): #4100 (series/9.x) and its cherry-picks #4106 (series/8.x) and #4105 (series/7.x), each carrying the golden test red-without/green-with. Triggers: #4106 / #4100 released ⇒ bumpelastic84s/elastic94s, retire the es8/es9 handler; #4105 released ⇒ bumpelastic74s(note the coordinate change tonl.gn0s1sfrom 7.17.5) and lift the es7 refusal; es6 never.Release-note items (0.23.0)
STDDEV,STDDEV_SAMP,STDDEV_POP,VARIANCE,VAR_SAMP,VAR_POPover a transformed expression (STDDEV(YEAR(x)),VARIANCE(ABS(x)), plain orOVER (PARTITION BY …)) now REFUSE with a 400 naming the release. They used to return the statistic of the raw field (or fail at Elasticsearch with an emptyextended_stats). Aggregate over a raw field there, or use ES 8+. (The Single-table MATERIALIZED VIEW rejected with internal error 'Number of stages must be positive' #185 precedent: honest rejection replacing silent acceptance.)ElasticSearchRequestgains a defaulted last field (serializer), andrequestToElasticSearchRequest/sqlQueryToAggregationseach gain a defaulted implicit parameter — all three source-compatible, all three binary-incompatible (arity). Downstream rebuilds on0.23.0.failOnWindowError = false) now builds its baseElasticQuerywithsql = Some(baseQuery.sql)like the success arm — one shared, eagerly-built value; the core SQL log line for that arm gains the statement text.GatewayApi.runas anElasticFailure(400) instead of escaping as an exception — REPL, JDBC and Arrow see the message.Docs
documentation/sql/functions_aggregate.md(STDDEV / VARIANCE family — "Transformed operands are per-major" + table),documentation/sql/dql_statements.md(one paragraph),documentation/sql/known_limitations.md(new section). MDX twins to sync by the web maintainer:softclient4es-web/src/content/docs/sql/functions-aggregate.mdx(or wherever the STDDEV family lives),sql/dql.mdx,sql/known-limitations.mdx— not edited here.Test evidence
Evidence (final tree, rebased onto BIDC-2's final head
b5a86cf9):+ softclient4es-sql-bridge/test(template, ES 8 elastic4s)ExtendedStatsEmissionSpec16 + the marker-tree null-safety guard inAggregationNamingSpec)+ es6bridge/test(hand-maintained)softclient4es7-sql-bridge/test,softclient4es8-sql-bridge/test(generated)softclient4es9-sql-bridge/test(generated,sbt17)+ core/testGatewayRefusalBoundarySpec6: SearchExecutor aggregation shape, SearchExecutor LIMIT row, CoreDqlExtension quota-capped scroll, windowed un-LIMITed scroll — all answered atrun—, non-refusing control, plus an extension deferring its refusal into its own Future)es8java/es9javaJavaClientExtendedStatsEmissionSpec(sbt17, Docker-free)NotImplementedErrorpreserved for an aggregation nobody can buildes7rest/es6rest/es6jest*ExtendedStatsRejectionSpec(sbt17, Docker-free)MAX(YEAR(x))keeps its script+ softclient4es-sql-bridge/compile+ es6bridge/compile+ core/compile+++ 2.12.20 <bridge, es6bridge, core, es8java, es7rest, es6rest, es6jest>/Test/compile+++ 2.12.20 softclient4es-core-testkit/compile— 11/11 greenscalafmtCheckAllgreen; CompileheaderCheckgreen on all 9 modules;es7rest/Test/headerCheckred on 12 PRE-EXISTING headerless specs only (none new; every new client-module spec carries the header)Docker (
sbt17, one major per invocation,WindowFunctionSpec= 38 existing + 2 new #222 cases):es8javaOVER (PARTITION BY department): the same sd per department on every row. The raw-field statistic (STDDEV(salary)19334.15, andhire_datein millis) sits beside them, unchangedes9javaes7restclient.run→ElasticFailure(400, "…not supported on Elasticsearch 7 … (elastic4s#4100)…")for the plain AND the windowed statement; the direct API throws the sameElasticErrores6restassume(sample keys need ES 7.7+)es6jestFinal-base re-runs (after the last rebase onto
b5a86cf9): bridge template 174/174 ×2 legs, es6 bridge 174/174 ×2 legs, core 873/873 ×2 legs (+ …/test); generated es7 + es8 174/174 (both invocations); generated es9 (sbt17) 172/174 on the FIRST invocation — the twoIN over an aggregatepins BIDC-2'sb09aaf27rewrote, compared against the stale previous test copy (the documented race) — then 174/174 on the second; es8JavaClientExtendedStatsEmissionSpec8/8; ES 8.18.3WindowFunctionSpec40/40.Falsification: es8 handler's
scriptline removed ⇒JavaClientExtendedStatsEmissionSpec6/8 RED (the two survivors are the parity + injection checks); restored, green.Review:
bmad-code-review's three layers run inline by the implementer (no reviewer sub-agent may be spawned per the run brief) — 4 findings applied during the run (boundary moved fromSearchExecutortoGatewayApi.run; lazy windowed translation hoisted; a test-scope implicit silently falling back to the Default serializer; an over-strict null-safety proxy replaced by verbatim marker-script equality); no open HIGH/MEDIUM. No independent fresh-context reviewer ran on this diff.Post-review re-runs (on
10f3f8fc, after the R3 follow-up commit):scalafmtCheckAllgreen; bridge template 176/176 ×2 legs; es6 bridge 176/176 ×2 legs; core 874/874 ×2 legs; generated es7 + es8 176/176 (both invocations each); generated es9 (sbt17) 176/176 (both); es8 + es9JavaClientExtendedStatsEmissionSpec9/9 each (the R3-1 diagnostic test included);GatewayRefusalBoundarySpec6/6 (the deferring-extension route included); ES 8.18.3WindowFunctionSpec40/40 on real Elasticsearch.🤖 Generated with Claude Code