You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #370 (PR pending). The lead's decision: all of these go into ONE PR — not one issue each. Every item below is pre-existing on main, was surfaced by executing the emissions of #370's shapes on real Elasticsearch, and was deliberately left out of #370 to keep it to parameter identity. None was made silent by #370; three went from silently wrong to loud.
LocalTime has no isEqual — CAST(d AS TIME) = CAST('00:00:00' AS TIME) fails dynamic method [java.time.LocalTime, isEqual/1] not found; the = spelling must be equals for a LocalTime.
CASE-expression WHEN has no null guard on its RIGHT operand — CASE <expr> WHEN lastSeen THEN … over a document without lastSeen: NullPointerException: ChronoLocalDate.toEpochDay() because "other" is null.
Column-vs-column temporal comparison translated as a range with the other column's NAME as date math — WHERE d > DATE_TRUNC(ts, MONTH) becomes {"range":{"d":{"gt":"ts||/M"}}} → parse_exception: failed to parse date field [ts]. Never reaches Painless; needs the script path.
Arithmetic over date functions drops the functions — WHERE YEAR(d) * 100 + MONTH(d) = 202501 renders param2 * 100 + param1 over the RAW doc-values (query context), and SCRIPT AS (YEAR(d) * 100 + MONTH(d)) does the same in an ingest processor (ArithmeticExpression.toPainless reads ctx.get(left) before the chain is applied).
Processor / Transform contexts still collapse — identifiers register as LiteralParam(processParamName), so contextKey never applies: SCRIPT AS (CASE WHEN YEAR(d) > MONTH(d) …) emits ….get(ChronoField.YEAR).get(ChronoField.MONTH_OF_YEAR) on ONE parameter; GREATEST(YEAR(d), MONTH(d)) likewise. Extensions' FieldAnalyzer renders through PainlessContext(Processor), so materialized-view enrichment inherits it.
Arg-coercion path re-narrows an already narrowed parameter — DATE_FORMAT(CAST(d AS DATE), …) / YEAR(CAST(d AS DATE)) inside a CASE keyed on the same CAST: function/package.scala appends painlessUtcLocalDate onto a parameter that already rendersLocalDate.
A second identifier INSTANCE's narrowing is lost when it shares a registered parameter — the order of two predicates in one script decides whether the shared parameter carries the .toLocalDate() (loud in one order, loud differently in the other). Carrying a method from a later instance onto the registered object is the fix.
Duplicated guarded expression — DATE_ADD(DATE_PARSE(name, …), INTERVAL 1 DAY) > d renders the guarded operand twice inside a != null ? … : null (valid, but every function in the chain is evaluated twice per document).
Also owed
The Where translator splits AND into separate script filters; CASE/COALESCE/Criteria.painless render one script. Rule 1's population statement should say so wherever it is documented.
Follow-up to #370 (PR pending). The lead's decision: all of these go into ONE PR — not one issue each. Every item below is pre-existing on
main, was surfaced by executing the emissions of #370's shapes on real Elasticsearch, and was deliberately left out of #370 to keep it to parameter identity. None was made silent by #370; three went from silently wrong to loud.Wrong-answer or loud, measured on ES 8.18.3
LocalDate/ZonedDateTimecomparison — a DATE-literal narrowing makes one side aLocalDatewhile the other side stays the rawZonedDateTime:CAST(d AS DATE) - INTERVAL 3 DAY > d(Cannot cast ZonedDateTime to ChronoLocalDate),DATE_TRUNC(d, MONTH) = CAST('2025-01-01' AS DATE) AND DATE_TRUNC(d, MONTH) < drendered as ONE script (CASE,Criteria.painless), no-schemaCAST(d AS DATE) = d(silently no rows). Six time functions emit a statement-sequence Painless — broken in predicates (silent zero rows for IN), and LAST_DAY fails even in SELECT #367's rule 8 for a COLUMN right operand. Before A column compared with a function of itself collapses onto one Painless parameter — silent wrong answer #370 these werex < x, silently false.LocalTimehas noisEqual—CAST(d AS TIME) = CAST('00:00:00' AS TIME)failsdynamic method [java.time.LocalTime, isEqual/1] not found; the=spelling must beequalsfor aLocalTime.WHENhas no null guard on its RIGHT operand —CASE <expr> WHEN lastSeen THEN …over a document withoutlastSeen:NullPointerException: ChronoLocalDate.toEpochDay() because "other" is null.rangewith the other column's NAME as date math —WHERE d > DATE_TRUNC(ts, MONTH)becomes{"range":{"d":{"gt":"ts||/M"}}}→parse_exception: failed to parse date field [ts]. Never reaches Painless; needs the script path.WHERE YEAR(d) * 100 + MONTH(d) = 202501rendersparam2 * 100 + param1over the RAW doc-values (query context), andSCRIPT AS (YEAR(d) * 100 + MONTH(d))does the same in an ingest processor (ArithmeticExpression.toPainlessreadsctx.get(left)before the chain is applied).LiteralParam(processParamName), socontextKeynever applies:SCRIPT AS (CASE WHEN YEAR(d) > MONTH(d) …)emits….get(ChronoField.YEAR).get(ChronoField.MONTH_OF_YEAR)on ONE parameter;GREATEST(YEAR(d), MONTH(d))likewise. Extensions'FieldAnalyzerrenders throughPainlessContext(Processor), so materialized-view enrichment inherits it.DATE_FORMAT(CAST(d AS DATE), …)/YEAR(CAST(d AS DATE))inside a CASE keyed on the same CAST:function/package.scalaappendspainlessUtcLocalDateonto a parameter that alreadyrendersLocalDate..toLocalDate()(loud in one order, loud differently in the other). Carrying a method from a later instance onto the registered object is the fix.DATE_ADD(DATE_PARSE(name, …), INTERVAL 1 DAY) > drenders the guarded operand twice inside a!= null ? … : null(valid, but every function in the chain is evaluated twice per document).Also owed
Wheretranslator splitsANDinto separate script filters; CASE/COALESCE/Criteria.painlessrender one script. Rule 1's population statement should say so wherever it is documented.caseWhenExprbridge fixture: its SCHEMA rendering fails (Class.castatparam2.isEqual(param5)—CURRENT_DATE - 7coerced toZonedDateTimeagainst aLocalDatechain, Six time functions emit a statement-sequence Painless — broken in predicates (silent zero rows for IN), and LAST_DAY fails even in SELECT #367'schainTypegap); only the no-schema bytes are pinned.Found 2026-09-19 while fixing #370; each item was executed on real Elasticsearch, before and after.