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
{{ message }}
Repository navigation
ES 6.8: a raw date doc-value reaches Painless un-normalised — DATE_FORMAT/DATETIME_FORMAT in every venue, and any temporal expression compared with a date column #371
On Elasticsearch 6.8 a date doc-value is an org.elasticsearch.script.JodaCompatibleZonedDateTime,
not a java.time.ZonedDateTime. Two emission sites hand that value straight to java.time API, so
they fail the shard on 6.8 while working on 7.17 / 8.18 / 9.0.
Measured on real Elasticsearch 6.8.23
Index holding d = 2025-01-06:
(a) DATE_FORMAT / DATETIME_FORMAT, in EVERY venue — the operand is passed as an ARGUMENT, so
it never reaches the UTC normalisation the CHAINED date functions get:
The same script over …toInstant().atZone(ZoneId.of('Z')) answers 2025-01-06 on 6.8.
(b) any temporal expression compared with a raw date COLUMN — the right-hand operand is rendered
as the bare doc-value:
WHERE LAST_DAY(d) = d
class_cast_exception: Cannot cast org.elasticsearch.script.JodaCompatibleZonedDateTime
to java.time.chrono.ChronoZonedDateTime (at left1.isEqual(param1))
Not a regression
Both fail identically before and after #367. DATE_FORMAT's projection has never worked on 6.8, and
before #367 the predicate form failed too (the dropped transform left param1.isEqual(param1), whose
ARGUMENT is the same un-normalised value).
How simple is the correction?
(a) is one line, at two sites, and it is MEASURED to work. In DateFormat.toPainlessCall and DateTimeFormat.toPainlessCall (sql/.../function/time/package.scala), the operand match gains a
temporal arm:
Verified on real 6.8.23 (2025-01-06) and a no-op on 7+, where the doc-value is already a UTC ZonedDateTime. Cost: it moves three pinned expectations — two in StandaloneCallAssemblySpec
(including one named "should be byte-identical to before the fix", from story 21.8 Part C) and one in
the #367 census — plus any downstream fixture pinning that emission.
(b) is NOT measured. It needs the same normalisation on the right-hand operand's rendering, which
is a different code path, and the blast radius there is unknown.
Decision requested
Given (a) is simple and proven while (b) is not: fix (a), and DOCUMENT the 6.8 limitation for the
shapes not fixed. The documentation touch-points are the DATE_FORMAT / DATETIME_FORMAT function
pages and the ES-version support matrix, which today claim these functions work on every supported
major.
Guards that exist today
PredicateFunctionResultSpec (added with #367) skips exactly these rows on 6.8 via assume, each
carrying the verbatim Elasticsearch error in a comment, so the boundary is recorded in the suite
rather than hidden — and turns into a real assertion the day it is fixed.
⚠️ Correction — half (a) is NOT simple, and the recommendation changes
I claimed above that (a) is "one line at two sites and MEASURED to work". That claim came from testing
the ONE shape the defect report names — a bare date column — and it does not survive enumerating the
other receivers that reach the same call site. Measured, on the emission of every documented shape:
shape
receiver the operand holds
one-line normalisation
DATE_FORMAT(d, 'yyyy')
raw doc-value
fixes it
DATETIME_FORMAT(ts, 'yyyy')
raw doc-value
fixes it
DATE_FORMAT(DATE_TRUNC(d, MONTH), 'yyyy')
LocalDate
breaks it
DATE_FORMAT(DATE_ADD(d, INTERVAL 1 DAY), 'yyyy')
LocalDate
breaks it
DATE_FORMAT(CAST(d AS DATE), 'yyyy')
LocalDate
breaks it
DATE_FORMAT(CURRENT_DATE, 'yyyy')
LocalDate
breaks it
DATE_FORMAT(LAST_DAY(d), 'yyyy')
LocalDate
breaks it
java.time.LocalDate has no toInstant(), so appending the UTC normalisation unconditionally trades
one broken receiver for five — the same trap #368 recorded, hit again by testing only the shape in the
report.
The actual mechanism, found by instrumenting the call site
The narrowing is already REQUESTED for the bare-column case and never reaches the emission.
Instrumenting DateFormat.toPainlessCall for DATE_FORMAT(d, 'yyyy') prints:
— no methods at all. So the operand's own chain asks for the normalisation, and the parameter
declaration is rendered without it. PainlessParam identity is the doc-value STRING, so the object
whose method list is mutated is not necessarily the object the prologue renders.
That is the same parameter-identity defect as #370, in a different disguise: one fix, in how a
parameter's declaration is rendered, addresses both. Appending a second normalisation at the call site
papers over it and, as measured above, breaks five working shapes.
Revised recommendation
Do NOT take the one-liner. Either fix parameter identity (with #370, and re-measure this family
against it), or document the ES 6.8 limitation for DATE_FORMAT / DATETIME_FORMAT on the
function pages and the version-support matrix, as originally directed.
Nothing for this was merged: the change was written, measured, refuted and reverted; #372 carries none
of it. The 6.8 rows stay assume-skipped in PredicateFunctionResultSpec with the verbatim
Elasticsearch errors.
Re-measured on Elasticsearch 6.8.23: main (f63222d) and #412's head (0687815)
On 6.8, #412 makes a computed date compared with a date column run, which is half (b) of this issue. Half (a) and three neighbouring shapes still fail. Everything was executed through the gateway. On 8.18.3, every statement below answers correctly on both builds; that is the control.
On an index mapped by hand, as in PredicateFunctionResultSpec: d and ts are date, seven documents dated 2025-01-06 … 12, and ts is a year later for d6.
Cannot cast …JodaCompatibleZonedDateTime to java.time.chrono.ChronoZonedDateTime
r1 r2 r3 r5
r1 r2 r3 r5
WHERE CAST(ts AS DATE) = d
same
r1 r2 r3 r5
r1 r2 r3 r5
WHERE DATE_ADD(d, INTERVAL 1 DAY) = d2
same
r1
r1
WHERE COALESCE(d, d2) > CAST('2024-01-01' AS DATE)
Cannot cast java.time.ZonedDateTime to org.elasticsearch.script.JodaCompatibleZonedDateTime
r1 r2 r4 r5
r1 r2 r4 r5
WHERE d2 = d + 1
Cannot apply [+] operation to types [org.elasticsearch.script.JodaCompatibleZonedDateTime] and [java.lang.Integer]
r1
r1
WHERE LAST_DAY(d) = d
cast error as above
r1 r5
r1 r5
WHERE DATE_TRUNC(d, MONTH) < d
cast error as above
r1 r3 r5
r1 r3 r5
Still fails on 6.8, on both builds
statement
Elasticsearch's error
WHERE DATE_FORMAT(d, 'yyyy') = '2025', its IN form, DATETIME_FORMAT, the projection SELECT DATE_FORMAT(d, 'yyyy-MM-dd'), and DATE_FORMAT(d, 'yyyy') = DATE_FORMAT(ts, 'yyyy')
Cannot cast org.elasticsearch.script.JodaCompatibleZonedDateTime to java.time.temporal.TemporalAccessor
WHERE CAST(d AS TIME) = CAST('00:00:00' AS TIME), its <> and + INTERVAL 1 HOUR > forms
dynamic method [org.elasticsearch.script.JodaCompatibleZonedDateTime, toLocalTime/0] not found
WHERE d2 > d, WHERE ts > d, WHERE d2 > DATE_TRUNC(d, MONTH)
400 failed to parse date field [d] with format [strict_date_optional_time||epoch_millis]: Parse failure at index [0] of [d]
SELECT id, CAST(TRUE AS INT) AS v FROM t
compile error Extraneous conditional statement
A date column compared with another bare date column fails for a different reason than this issue. The ES 6 bridge renders the comparison as a range query whose bound is the other column's name: {"range":{"d2":{"gt":"d||"}}}, and {"gt":"d||/M"} for DATE_TRUNC(d, MONTH). Elasticsearch then parses d as a date. The ES 7 / 8 / 9 bridges render a script that compares the two doc values, and on 8.18.3 it gives PostgreSQL's rows: r1; r1 r5; r1 r2 r3. With fix(sql): date arithmetic follows the SQL engines; type checks wait for the column types #412, WHERE d2 > d + 0 runs on 6.8 (r1).
CAST(TRUE AS INT) renders (true ? 1 : 0), a constant conditional that Painless 6.8 refuses to compile. CAST(b AS INT) over the column runs (1, 0, NULL, 1, 0).
The five esMajor >= 7 cancels in PredicateFunctionResultSpec (#412's head)
runs and gives the asserted set (all but d6), on main too
Guards 303 and 431 no longer hold. These rows were measured through the gateway (GatewayApi.run). The spec's selected goes through the client venue, so re-enabling the two tests confirms them there.
Activity
fupelaqu commentedon Sep 19, 2026
I claimed above that (a) is "one line at two sites and MEASURED to work". That claim came from testing
the ONE shape the defect report names — a bare date column — and it does not survive enumerating the
other receivers that reach the same call site. Measured, on the emission of every documented shape:
DATE_FORMAT(d, 'yyyy')DATETIME_FORMAT(ts, 'yyyy')DATE_FORMAT(DATE_TRUNC(d, MONTH), 'yyyy')LocalDateDATE_FORMAT(DATE_ADD(d, INTERVAL 1 DAY), 'yyyy')LocalDateDATE_FORMAT(CAST(d AS DATE), 'yyyy')LocalDateDATE_FORMAT(CURRENT_DATE, 'yyyy')LocalDateDATE_FORMAT(LAST_DAY(d), 'yyyy')LocalDatejava.time.LocalDatehas notoInstant(), so appending the UTC normalisation unconditionally tradesone broken receiver for five — the same trap #368 recorded, hit again by testing only the shape in the
report.
The actual mechanism, found by instrumenting the call site
The narrowing is already REQUESTED for the bare-column case and never reaches the emission.
Instrumenting
DateFormat.toPainlessCallforDATE_FORMAT(d, 'yyyy')prints:while the declaration the script actually carries is
— no methods at all. So the operand's own chain asks for the normalisation, and the parameter
declaration is rendered without it.
PainlessParamidentity is the doc-value STRING, so the objectwhose method list is mutated is not necessarily the object the prologue renders.
That is the same parameter-identity defect as #370, in a different disguise: one fix, in how a
parameter's declaration is rendered, addresses both. Appending a second normalisation at the call site
papers over it and, as measured above, breaks five working shapes.
Revised recommendation
Do NOT take the one-liner. Either fix parameter identity (with #370, and re-measure this family
against it), or document the ES 6.8 limitation for
DATE_FORMAT/DATETIME_FORMATon thefunction pages and the version-support matrix, as originally directed.
Nothing for this was merged: the change was written, measured, refuted and reverted; #372 carries none
of it. The 6.8 rows stay
assume-skipped inPredicateFunctionResultSpecwith the verbatimElasticsearch errors.
docs(sql): record the Elasticsearch 6.8 limit of DATE_FORMAT / DATETI…
fupelaqu commentedon Oct 6, 2026
Re-measured on Elasticsearch 6.8.23:
main(f63222d) and #412's head (0687815)On 6.8, #412 makes a computed date compared with a date column run, which is half (b) of this issue. Half (a) and three neighbouring shapes still fail. Everything was executed through the gateway. On 8.18.3, every statement below answers correctly on both builds; that is the control.
Runs on 6.8 with #412, fails on
mainOn an index mapped by hand, as in
PredicateFunctionResultSpec:dandtsaredate, seven documents dated 2025-01-06 … 12, andtsis a year later ford6.mainWHERE LAST_DAY(d) = dCannot cast org.elasticsearch.script.JodaCompatibleZonedDateTime to java.time.chrono.ChronoZonedDateTimeWHERE LAST_DAY(d) > dWHERE DATE_TRUNC(d, MONTH) < dWHERE DATE_ADD(d, INTERVAL 1 DAY) = dOn a table declared through elasticsql (
d,d2DATE,tsTIMESTAMP; five rows), compared with PostgreSQL 16.14:mainWHERE CURRENT_DATE > dCannot cast …JodaCompatibleZonedDateTime to java.time.chrono.ChronoZonedDateTimeWHERE CAST(ts AS DATE) = dWHERE DATE_ADD(d, INTERVAL 1 DAY) = d2WHERE COALESCE(d, d2) > CAST('2024-01-01' AS DATE)Cannot cast java.time.ZonedDateTime to org.elasticsearch.script.JodaCompatibleZonedDateTimeWHERE d2 = d + 1Cannot apply [+] operation to types [org.elasticsearch.script.JodaCompatibleZonedDateTime] and [java.lang.Integer]WHERE LAST_DAY(d) = dWHERE DATE_TRUNC(d, MONTH) < dStill fails on 6.8, on both builds
WHERE DATE_FORMAT(d, 'yyyy') = '2025', itsINform,DATETIME_FORMAT, the projectionSELECT DATE_FORMAT(d, 'yyyy-MM-dd'), andDATE_FORMAT(d, 'yyyy') = DATE_FORMAT(ts, 'yyyy')Cannot cast org.elasticsearch.script.JodaCompatibleZonedDateTime to java.time.temporal.TemporalAccessorWHERE CAST(d AS TIME) = CAST('00:00:00' AS TIME), its<>and+ INTERVAL 1 HOUR >formsdynamic method [org.elasticsearch.script.JodaCompatibleZonedDateTime, toLocalTime/0] not foundWHERE d2 > d,WHERE ts > d,WHERE d2 > DATE_TRUNC(d, MONTH)failed to parse date field [d] with format [strict_date_optional_time||epoch_millis]: Parse failure at index [0] of [d]SELECT id, CAST(TRUE AS INT) AS v FROM tExtraneous conditional statement{"range":{"d2":{"gt":"d||"}}}, and{"gt":"d||/M"}forDATE_TRUNC(d, MONTH). Elasticsearch then parsesdas a date. The ES 7 / 8 / 9 bridges render a script that compares the two doc values, and on 8.18.3 it gives PostgreSQL's rows: r1; r1 r5; r1 r2 r3. With fix(sql): date arithmetic follows the SQL engines; type checks wait for the column types #412,WHERE d2 > d + 0runs on 6.8 (r1).CAST(TRUE AS INT)renders(true ? 1 : 0), a constant conditional that Painless 6.8 refuses to compile.CAST(b AS INT)over the column runs (1, 0, NULL, 1, 0).The five
esMajor >= 7cancels inPredicateFunctionResultSpec(#412's head)DATE_FORMAT(d, …) = / IN …DATETIME_FORMAT(d, …) = …LAST_DAY(d) = d,LAST_DAY(d) > d,DATE_TRUNC(d, MONTH) < d,DATE_ADD(d, INTERVAL 1 DAY) = d,DATE_TRUNC(d, QUARTER) < dCAST(d AS TIME) …LAST_DAY(d) = LAST_DAY(ts)maintooGuards 303 and 431 no longer hold. These rows were measured through the gateway (
GatewayApi.run). The spec'sselectedgoes through the client venue, so re-enabling the two tests confirms them there.