-
Notifications
You must be signed in to change notification settings - Fork 25.1k
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
ESQL: Add ability to perform date math #98870
Conversation
This adds the ability to perform date math: allow date types be updated with a time span (a duration or a period). Ex. : `row n = now() | eval then = n - 1 year + 2 minutes`
Hi @bpintea, I've created a changelog YAML for you. |
Pinging @elastic/es-ql (Team:QL) |
Pinging @elastic/elasticsearch-esql (:Query Languages/ES|QL) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, I only have minor remarks which are all meant to be optional.
Additionally, maybe it would make sense to add the following tests:
- unit tests tests for
period + datetime
andduration + datetime
- unit tests for
period - datetime
andduration - datetime
- unit or csv tests for weird edge cases, like date math for leap years, adding days or hours past the end of a month, etc. Although this may be less important since we know that we can trust the date math functions from the
ql
package.
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypes.java
Outdated
Show resolved
Hide resolved
...csearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java
Outdated
Show resolved
Hide resolved
...csearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java
Outdated
Show resolved
Hide resolved
...csearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java
Show resolved
Hide resolved
if (EsqlDataTypes.isDateTime(leftType) || EsqlDataTypes.isDateTime(rightType)) { | ||
Expression dateTime = argumentOfType(dt -> dt == DataTypes.DATETIME); | ||
Expression nonDateTime = argumentOfType(dt -> dt != DataTypes.DATETIME); | ||
if (dateTime == null || nonDateTime == null || EsqlDataTypes.isTemporalAmount(nonDateTime.dataType()) == false) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: instead of using nonDateTime
together with isTemporalAmount
, wouldn't it be easier to just use argumentOfType(EsqlDataTypes::isTemporalAmount
, like below in line 73?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main comment I have is around the use of timezones.
Date functions need to be time zone aware yet here the UTC tz is used - that makes sense only if the dates are considered to be coming from ES and thus they originate in UTC but the output is converted into the user TZ.
If that is not the case, this should be clarified.
Lastly some docs help - I would reuse the structure and content from SQL since its fairly similar.
|
||
@Evaluator(extraName = "Datetimes", warnExceptions = { ArithmeticException.class, DateTimeException.class }) | ||
static long processDatetimes(long datetime, @Fixed TemporalAmount temporalAmount) { | ||
return asMillis(asDateTime(datetime, DEFAULT_TZ).plus(temporalAmount)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use UTC and not the request timezone?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a comment.
Both timestamps that come from ES as well as those converted by our functions are in UTC.
@@ -52,6 +52,14 @@ public static ZonedDateTime asDateTime(long millis) { | |||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), UTC); | |||
} | |||
|
|||
public static ZonedDateTime asDateTime(long millis, ZoneId zoneId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you always pass UTC, why not use the above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remanent from a first attempt to use session's zoneID. Updated to use the existing one.
Add Add/Sub unit tests Make temporal units negatable. Rename data type tests methods. More tests.
I've added a comment.
Will follow with a subsequent PR. |
added.
Good point, thanks. Increased the Sub type resolution checks to prevent this.
Indeed, left this out. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still LGTM, only added very minor remarks (all supposed to be completely optional). Did not look too closely into what appeared to be refactors of the test that probably became necessary due to #98890.
@@ -85,6 +85,7 @@ static double processDoubles(double lhs, double rhs) { | |||
|
|||
@Evaluator(extraName = "Datetimes", warnExceptions = { ArithmeticException.class, DateTimeException.class }) | |||
static long processDatetimes(long datetime, @Fixed TemporalAmount temporalAmount) { | |||
return asMillis(asDateTime(datetime, DEFAULT_TZ).plus(temporalAmount)); | |||
// using a UTC conversion since `datetime` is always a UTC-Epoch timestamp, either read from ES or converted through a function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: very helpful comment.
@@ -112,14 +137,47 @@ public void testEdgeCases() { | |||
|
|||
return; | |||
} | |||
if (testCaseType == EsqlDataTypes.DATE_PERIOD) { | |||
Period minPeriod = Period.of(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: good call testing the Neg
edge cases :)
...est/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/NegTests.java
Show resolved
Hide resolved
...est/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubTests.java
Show resolved
Hide resolved
...est/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddTests.java
Show resolved
Hide resolved
if (false == (lhsType == rhsType || lhsType.isNumeric() && rhsType.isNumeric())) { | ||
return false; | ||
} | ||
return super.supportsTypes(lhsType, rhsType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question (bikeshed-y, so not meant as a suggestion in any way): couldn't this just be
if (false == (lhsType == rhsType || lhsType.isNumeric() && rhsType.isNumeric())) { | |
return false; | |
} | |
return super.supportsTypes(lhsType, rhsType); | |
return (lhsType == rhsType || lhsType.isNumeric() && rhsType.isNumeric())) | |
&& super.supportsTypes(lhsType, rhsType); |
Address review comments
This adds the ability to perform date math: allow date types be updated with a time span (a duration or a period).
Ex. :
row n = now() | eval then = n - 1 year + 2 minutes
Closes #98402.