Skip to content

A column compared with a function of itself collapses onto one Painless parameter — silent wrong answer #370

Closed

Description

@fupelaqu

A predicate that compares a column with a FUNCTION OF THAT SAME COLUMN answers with the wrong rows,
silently, on every Elasticsearch major.

Measured on real Elasticsearch 8.18.3

Two documents, d = 2025-01-06 and d = 2024-01-06:

WHERE DATE_TRUNC(d, MONTH) < d        rows: []            truth: both documents
WHERE DATE_ADD(d, INTERVAL 1 DAY) = d rows: [a, b]        truth: neither

HTTP 200 both times. The first is always false, the second always true, whatever the data.

Cause

PainlessParam identity is the doc-value STRING (Identifier.param = paramName), while the
function's methods are attached to the parameter OBJECT by addPainlessMethod. So when the same
column appears on both sides and the left-hand function folds into the parameter as a method, both
sides resolve to that ONE parameter — and the emitted script compares the transformed value with
itself:

WHERE DATE_TRUNC(d, MONTH) < d
  def param1 = (doc['d'].size() == 0 ? null
              : doc['d'].value.toInstant().atZone(ZoneId.of('Z')).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS));
  def left1 = param1;
  (left1 == null || param1 == null ? false : ((left1.isBefore(param1))))

param1 carries the truncation, so left1.isBefore(param1) is x < x.

Population

Exactly the functions whose Painless folds into the parameter as a suffix method — DATE_TRUNC,
DATE_ADD / DATE_SUB and the interval family, YEAR, MONTH, DAY, EPOCHDAY, the whole
extraction family — compared against their own column.

A function that renders as an EXPRESSION is unaffected, because it never folds: WHERE UPPER(name) = name emits left1 == param1 with left1 = param1.toUpperCase() and is correct (measured: returns
the right row).

Not a regression

Present before and after the fix for #367, verbatim the same rows. #367 removed the shortcut that
dropped the transform; it does not touch parameter identity.

Why it was not fixed with #367

The repair is to parameter IDENTITY — two uses of one column with different transforms must be two
parameters — and every emission that reuses a parameter goes through that code (YEAR(d) = 2025
included). That is its own change with its own evidence, not a rider on a predicate fix.

Guard that exists today

PredicateFunctionResultSpec (added with #367) executes DATE_TRUNC(d, MONTH) < d on all four
majors and asserts the WRONG answer, with this issue's number in the comment, so the day it is fixed
that assertion turns red and says why.

Activity

  1. added 2 commits that reference this issue on Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions