Skip to content

Fix inconsistent roundup behavior for date ranges using Temporal object as terms #127739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

mosche
Copy link
Contributor

@mosche mosche commented May 6, 2025

roundUp works well for terms that contain date math expressions. Otherwise, if falling through to using roundUp parsers, the behavior can be surprising. Instead, these parsers fill missing date components with the max possible value, e.g. 2099-12 becomes 2099-12-01T23:59:59.999_999_999Z.

Prior to switching to Java time, Joda's LocalTime#toString() consistently formatted time using HH:mm:ss.SSS. Without and missing component, roundUp parsers behave just like normal parsers.

Java's LocalTime, however, outputs the shortest possible representation and might omit seconds, milliseconds, etc.
This interferes badly with the behavior of the roundup parser. And as a result of this, depending on the term, the lower bound could end up larger than the upper bound, e.g. 1970-01-06T09:49Z becomes 1970-01-06T09:49:59.999Z.

This PR disables roundUp parsers if the lower/upper term is a Temporal object to restore the behavior prior to switching to Java time.

Fixes #86284
Relates to #86508

…ct as terms

`roundUp` works well for terms that contain date math expressions.
Otherwise, if falling through to using roundUp parsers, the behavior can
be surprising. Instead, these parsers fill missing date components with
the max possible value, e.g. `2099-12` becomes
`2099-12-01T23:59:59.999_999_999Z`.

Prior to switching to Java time, Joda's LocalTime#toString()
consistently formatted time using `HH:mm:ss.SSS`. Without and missing
component, roundUp parsers behave just like normal parsers.

Java's LocalTime, however, outputs the shortest possible representation
and might omit seconds, milliseconds, etc.
This interferes badly with the behavior of the roundup parser. And as a
result of this, depending on the term, the lower bound could end up
larger than the upper bound, e.g. `1970-01-06T09:49Z`
becomes `1970-01-06T09:49:59.999Z`.

This PR disables roundUp parsers if the lower/upper term is a Temporal
object to restore the behavior prior to switching to Java time.
@mosche mosche added the >bug label May 6, 2025
@mosche mosche requested review from a team and cbuescher May 6, 2025 07:07
@elasticsearchmachine elasticsearchmachine added needs:triage Requires assignment of a team area label v9.1.0 labels May 6, 2025
@mosche mosche added Team:Core/Infra Meta label for core/infra team and removed needs:triage Requires assignment of a team area label labels May 6, 2025
@elasticsearchmachine elasticsearchmachine added needs:triage Requires assignment of a team area label and removed Team:Core/Infra Meta label for core/infra team labels May 6, 2025
@mosche mosche added the :Core/Infra/Core Core issues without another label label May 6, 2025
@elasticsearchmachine elasticsearchmachine added the Team:Core/Infra Meta label for core/infra team label May 6, 2025
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-core-infra (Team:Core/Infra)

@elasticsearchmachine elasticsearchmachine removed the needs:triage Requires assignment of a team area label label May 6, 2025
@elasticsearchmachine
Copy link
Collaborator

Hi @mosche, I've created a changelog YAML for you.

Copy link
Member

@cbuescher cbuescher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its been a long time since I've been doing any date rounding, so I'd prefer if someone from @elastic/es-core-infra that is currently doing date-math related stuff is taking a look here. I left a small question around maybe avoiding the second "instanceof" in some cases but for correctness would like to defer to another reviewer.

// `roundUp` may only be used if the term is not a Temporal object. LocalTime#toString() and similar will output
// the shortest possible representation and might omit seconds, milliseconds, etc.
// That interferes badly with the behavior of the roundup parser leading to unexpected results.
boolean mayRoundUp = (term instanceof Temporal) == false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the second "instanceof" check can be avoided in case we already know that term is a BytesRef from the previous check? Also if "roundUp" is already false, this check can be avoided (i.e. pull it into the second part of the short-circuit && or something similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx, I've addressed your comment

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an explicit test that demonstrates this bug? I know there are some previously muted tests, but those are testing other things that just happened to break here. An explicit test would make this fix more understandable.

@mosche
Copy link
Contributor Author

mosche commented May 7, 2025

Is there an explicit test that demonstrates this bug?

@rjernst I added two very explicit tests to demonstrate the issue:

  • testDateRangeQueryDoesNotRoundupTemporals demonstrates the bug, this fails without this fix
  • testRangeQueryIntersectsAdjacentDates demonstrates a valid (??? - well, at least it makes some sense in this case) usage of the roundup parsers

Copy link
Member

@cbuescher cbuescher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM for the instanceof checks I commented on earlier

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tests. I'm not sure about this change. The round up behavior is intentional, or at least by how we define round up at the moment. In the date math parser the flag is called roundUpIfNoTime. I wonder if the issue is there then, that we should not fill in nanos if milliseconds are already provided.


// explicitly not using nextFrom here to provide a concrete, visual example.
// see the behavioral difference compared to testRangeQueryIntersectsAdjacentDates
ZonedDateTime from = ZonedDateTime.parse("2025-05-01T14:10:00.000Z");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little deceptive, the parser used in rangeQuery will call toString on this. I think it would be slightly clearer if the test used Strings instead, which matches what would come from a real query, not a ZonedDateTime object.

I wonder if that is the issue with the original test failures: they're passing in ZonedDateTime objects and subject to their toString behavior, but in production this wouldn't happen, we get strings directly from the request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't care about ZonedDateTime objects, there's no problem here and the failing tests have simply tested for the wrong thing... the problem is really only the difference between Joda vs Java time behaving differently for the toString (pls read the PR description for the details)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I don't think ZonedDateTime is possible outside tests, see where eg from is set in RangeQueryBuilder, it basically comes down to parsing xcontent, where we only have strings and numbers. The entire point of the date math parser is to turn a string into a ZonedDateTime, so it doesn't make sense that we would try to take an already realized ZonedDateTime, turn it back into a string, and then get a ZonedDateTime.

ZonedDateTime from = ZonedDateTime.parse("2025-05-01T14:10:00.000Z");
ZonedDateTime to = ZonedDateTime.parse("2025-05-01T14:11:00.000Z");

// usage of roundUp parsers is wrong if terms are Temporal objects, otherwise it would arbitrarily change the bounds
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by "Temporal object" don't you mean all dates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>bug :Core/Infra/Core Core issues without another label Team:Core/Infra Meta label for core/infra team v9.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[CI] RangeFieldTypeTests testRangeQueryIntersectsAdjacentValues failing
4 participants