[Streams][Streamlang] Align ES|QL condition transpiler with Painless on null propagation#264751
Conversation
|
Pinging @elastic/obs-onboarding-team (Team:obs-onboarding) |
Scout Test ReviewNo issues found ✅ Share feedback in the #appex-qa channel. Posted via Macroscope — Scout Test Review |
ApprovabilityVerdict: Needs human review This PR changes how ES|QL and Painless transpilers evaluate conditions when fields are null/missing, altering runtime behavior in data processing pipelines. The author does not own any of the changed files, which belong to several different teams who should review this semantic change to their transpiler and query generation code. No code changes detected at You can customize Macroscope's approvability policy. Learn more. |
|
/ci |
flash1293
left a comment
There was a problem hiding this comment.
Tested and works as expected, @elastic/security-entity-analytics @elastic/core-analysis please make sure that this doesn't break your usage somehow.
Also FYI @elastic/obs-sig-events-team , not sure whether you rely on this code path
|
@romulets can you please have a look? |
Catch flakiness early (recommended)Recommended before merge: run the flaky test runner against this PR to catch flakiness early. Covers the new Trigger a run with the Flaky Test Runner UI or post this comment on the PR: Share feedback in the #appex-qa channel. Posted via Macroscope — Flaky Test Runner nudge |
|
/ci |
💛 Build succeeded, but was flaky
Failed CI StepsTest Failures
Metrics [docs]Async chunks
History
|
…ilder_new_vis_attachment * commit '6fd683609eb6dee81f242f8ff6951edbe3bfd66c': (226 commits) Remove Model Author group-by option from external inference endpoints (elastic#264761) [Streams][Streamlang] Align ES|QL condition transpiler with Painless on null propagation (elastic#264751) chore(axios,workflows-eng): remove axios from workflows connector utils (elastic#267512) [failed-test-reporter] avoid opening issues for scout env failures (elastic#267649) [kbn-api-contracts] Detect request-body additionalProperties:false tightening (elastic#267546) [main] Sync bundled packages with Package Storage (elastic#267644) Centralize phase colors and descriptions (elastic#266680) [Unified Waterfall] Add "Scroll to origin" button (elastic#266594) [APM] Add alert and SLO badges to the service map embeddable (elastic#266360) [CI] Speed up telemetry_check by pre-filtering to collector files (elastic#265978) [Discover] Address flaky large CSV test (elastic#266642) avoid passing unrelated props within integration card icon component conditional render (elastic#266569) [Cases][Templates] Extend cases search by template field label (elastic#266414) [Background search] Migrate custom SplitButton to EuiSplitButton (elastic#267447) [i18n] Report translation coverage during integrate (elastic#264124) [api-docs] 2026-05-05 Daily api_docs build (elastic#267639) [Scout] Update test config manifests (elastic#267636) [content list] Add saved object provider services (elastic#266428) [Fleet] Otel UI add health and implement it in OTelComponentDetail (elastic#267292) Update dependency msw to v2.13.4 (main) (elastic#266770) ...
…n alignment in maintainer ES|QL snapshots Refresh 7 of 8 maintainer ES|QL golden snapshots to track the upstream alignment of the ES|QL condition transpiler with Painless null propagation (PR elastic#264751, "[Streams][Streamlang] Align ES|QL condition transpiler with Painless on null propagation"). That change rewrote the output of `getEuidDocumentsContainsIdFilter`, `getFieldEvaluationsEsql`, and `euid.esql.getEuidEvaluation` (all from `@kbn/entity-store`) so that comparisons sitting under a NOT/AND/OR chain are now wrapped in `COALESCE(expr, TRUE | FALSE)`, preserving the same truth value when one of the operands is NULL. The previous, un-wrapped form silently evaluated NULL-on-either-side comparisons to NULL, which Painless treats as FALSE rather than three-valued — the upstream change brings ES|QL into line. The diff is 100% the COALESCE wrap (`grep -v COALESCE` over the added lines is empty); no column names, identifiers, FROM clauses, or query shapes changed. The azure_auditlogs override snapshot is unchanged (it does not consume the affected helpers). All 200 maintainer unit tests pass with the refreshed snapshots. Co-authored-by: Cursor <cursoragent@cursor.com>
Closes #260925
Summary
The Streamlang Painless and ES|QL transpilers disagreed on how a condition should behave when the field it references is missing or
NULL. Painless uses two-valued logic (null-guards every leaf, somissing == "active"→false), while ES|QL uses three-valued logic (so the same comparison →NULL, which then propagates throughNOT,AND,OR, andCASE). As a result, anelsebranch of{ if: foo == "active" } / { else: ... }silently disappeared in ES|QL whenfoowas missing — neither theifnor theelsefired, and the document came out unchangedThis PR closes that gap in two complementary changes:
COALESCE(<predicate>, <default>)soNULLleaves collapse to a decisive boolean beforeNOT/AND/OR/CASEsee them. Once every leaf is two-valued, every operator above it behaves classicallyFALSEfor positive leaves (eq,gt,range,contains, …) — a missing field "doesn't match".TRUEfor the negative leaf (neq) — a missing field "is not equal to" any concrete value. This makesneqandnot(eq)semantically equivalent on missing fields (bothTRUE) and aligns Streamlang with Query DSL (bool.must_not.term)The Painless transpiler is updated in lockstep so the two backends produce identical results: every
neqshorthand binary now emits a disjunctive null-guard (field === null || …) instead of the conjunctive one (field !== null && …). Every other leaf keeps the conjunctive guard.existsis explicitly not wrapped —IS NULLis a total predicate in ES|QL and never returnsNULLitself, so no wrapper is needed.Behavior changes (compiled ES|QL)
Positive leaves — wrapped with
FALSEdefault:Before:
After:
Negative leaves (
neq) — wrapped withTRUEdefault:Before:
After:
The
TRUEdefault makes a missingattributes.statussatisfy theneqpredicate, mirroringnot(eq), Painless, and Query DSLmust_not.termsemantics.Behavior changes (compiled Painless)
neqshorthand binaries swap their null-guard from conjunctive (!== null &&) to disjunctive (=== null ||):Every other leaf is unchanged
Tradeoff: Discover divergence
This change makes Streamlang
neqsemantics diverge from raw ES|QL!=semantics in Discover and other surfaces where users hand-write ES|QL:where: { neq: "deleted" }→ matches missing-field documents.WHERE field != "deleted"(typed in Discover) → excludes missing-field documents (native ES|QL three-valued logic).