-
Notifications
You must be signed in to change notification settings - Fork 25.6k
SQL: Introduce SQL TIME data type #39802
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
Conversation
|
Pinging @elastic/es-search |
Support ANSI SQL's TIME type by introductin a runtime-only ES SQL time type. Closes: elastic#38174
costin
left a comment
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.
Thanks for working on this.
I've left a first round of comments.
| 1955-01-21 00:00:00Z | Maliniak | ||
| ; | ||
|
|
||
| timeAsFilter_NoMatch |
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.
Please add a test with an ORDER BY on a time (datetime cast).
| protected abstract Object doFold(ZonedDateTime dateTime); | ||
|
|
||
|
|
||
| protected Object doFold(OffsetTime time) { |
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.
If the default implementation is to throw an exception in case of a time object, then the folding should not occur in the first place.
In other words, the subclasses that support time should extend the existing methods instead of the opposite, all classes need to override the default implementation which throws an exception about "time".
| return doProcess(((ZonedDateTime) input).withZoneSameInstant(zoneId)); | ||
| } | ||
| if (input instanceof OffsetTime) { | ||
| return doProcess(((OffsetTime) input).withOffsetSameInstant(ZoneOffset.UTC)); |
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 should the time be converted to UTC? The zoneId of the function should be used instead (potentially by converting it to a ZoneOffset or going through an Instant before).
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.
That's wrong, fixed and added tests.
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.
yep, that's wrong
| abstract Object doProcess(ZonedDateTime dateTime); | ||
| } | ||
|
|
||
| Object doProcess(OffsetTime time) { |
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.
Same as above - why go through the hassle of creating a time object if only to discard it in the doProcess.
| } | ||
|
|
||
| public static Integer dateTimeChrono(OffsetTime time, String tzId, String chronoName) { | ||
| return dateTimeChrono(time.withOffsetSameInstant(ZoneId.of(tzId).getRules().getOffset(Instant.now())), |
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 a ZoneId can be ZoneOffset, the conversion process should be promoted to a static method in DateUtils.
| public static final String EMPTY = ""; | ||
|
|
||
| private static final DateTimeFormatter ISO_WITH_MILLIS = new DateTimeFormatterBuilder() | ||
| private static final DateTimeFormatter ISO_DATE_WITH_MILLIS = new DateTimeFormatterBuilder() |
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.
Doesn't the JDBC driver have access to StringUtils proto? Meaning these formatters would not be needed anymore...
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.
Good catch, removing and using StringUtils.
| } | ||
|
|
||
| return l.plus(r); | ||
| static Temporal add(Temporal l, Period r) { |
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 delegate to periodArithmetics instead of having its content here?
Same applies to the other add and sub methods.
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.
Leftover of some intermediate state. will change.
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.
Ah, sorry no, this is because the periodArithmetics/durationArithmetics are also used for the minus.
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.
Temporal::plus/minus actually delegate to TemporalAmount::addTo/substractFrom which is what Duration and Period are. Considering the latter types are not used anymore, it makes sense to have just one method with Temporal and TemporalAmount and a boolean or enum to indicate whether it's addition or substraction.
It's easier and more accurate vs the closure which is too broad. I would still opt for type checks on either side to make sure no unknown objects are being sent.
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 type checks have been added to BinaryArithmeticOperation
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.
But how can I have one method since There is some difference on how Duration & Period are handled, I wouldn't like to make instance of on the TemporalAmount.
| } | ||
|
|
||
| if (l instanceof OffsetTime) { | ||
| r = r.withDays(0).withMonths(0).withYears(0); |
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.
This creates 2 temporary objects - use of or Instant instead.
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.
This is a Period, couldn't find an alternative way.
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.
Period contains years/months/days - if everything is zero then Period.ZERO should be returned.
Otherwise Period.of(int, int, int) works.
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 was confused that Period can also have hours,mins, etc..
Thx and apologies for that.
| } | ||
|
|
||
| return l.plus(r); | ||
| static Temporal add(Temporal l, Period r) { |
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.
Using Temporal is too broad without validation is dangerous. We actually accept only ZonedDateTime and OffsetTime. Ideally we should check this up to the source however this creates too many methods - yet at some point we need to do the "ugly" part of checking the actual types and throwing an exception otherwise.
| return DataType.DOUBLE; | ||
| case "date": | ||
| return DataType.DATE; | ||
| case "time": |
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 find time after datetime/timestamp" more intuitive.
astefan
left a comment
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.
Left few comments.
Also, you should add tests to SqlProtocolTestCase.
| .appendOffsetId() | ||
| .toFormatter(Locale.ROOT); | ||
|
|
||
| static final DateTimeFormatter ISO_WITH_MILLIS = new DateTimeFormatterBuilder() |
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 the other one is called ISO_DATE_WITH_MILLIS, wouldn't this be better off as ISO_TIME_WITH_MILLIS?
| FROM "test_emp" WHERE emp_no < 10010 ORDER BY emp_no; | ||
|
|
||
| d:i | m:i | h:i | ||
| 0 |0 |0 |
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.
TIME tests with data from test_emp are kind of half useful. Should we consider updating our test data?
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.
Yep, will do it in an upcoming PR if you don't mind.
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.
For the moment I added tests here: https://github.com/elastic/elasticsearch/pull/39802/files#diff-a8ce71426601af8d094e40cafc9a4418
| .appendOffsetId() | ||
| .toFormatter(Locale.ROOT); | ||
|
|
||
| private static final DateTimeFormatter ISO_WITH_MILLIS = new DateTimeFormatterBuilder() |
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.
Maybe ISO_TIME_WITH_MILLIS?
|
|
||
| public static TypeResolution isNumericOrDate(Expression e, String operationName, ParamOrdinal paramOrd) { | ||
| return isType(e, dt -> dt.isNumeric() || dt.isDateBased(), operationName, paramOrd, "date", "datetime", "numeric"); | ||
| public static TypeResolution isDateOrTime(Expression e, String operationName, ParamOrdinal paramOrd) { |
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.
isTemporal sounds better?
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.
Same here.
| return isType(e, dt -> dt.isDateBased() || dt.isTimeBased(), operationName, paramOrd, "date", "time", "datetime"); | ||
| } | ||
|
|
||
| public static TypeResolution isNumericOrDateOrTime(Expression e, String operationName, ParamOrdinal paramOrd) { |
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.
isNumericOrTemporal sounds better?
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.
Imho it's more clear like it is now.
| @Override | ||
| protected TypeResolution resolveWithIntervals() { | ||
| if (right().dataType().isDateBased() && DataTypes.isInterval(left().dataType())) { | ||
| if ((right().dataType().isDateBased() || right().dataType().isTimeBased()) && DataTypes.isInterval(left().dataType())) { |
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.
There are many calls for dataType().isDateBased() || dataType().isTimeBased(). I think it's worth adding to DataType a method that should do this check. Maybe called isTemporalBased?
| ex.getMessage()); | ||
| } | ||
|
|
||
| public void testTimeLiteralUnsupported() { |
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.
This method shouldn't be called like this anymore.
| } | ||
|
|
||
| private static Integer dateTimeChrono(ZonedDateTime dateTime, ChronoField field) { | ||
| private static Integer dateTimeChrono(Temporal dateTime, ChronoField field) { |
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.
Isn't Temporal too broad here?
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.
It's an internal method, called only with ZonedDateTime or OffsetTime so it's not dangerous and avoids duplication.
|
@costin @astefan
|
|
@elasticmachine run elasticsearch-ci/2 |
astefan
left a comment
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.
Looking good. Left two comments.
| protected DateTimeProcessor mutateInstance(DateTimeProcessor instance) { | ||
| DateTimeExtractor replaced = randomValueOtherThan(instance.extractor(), () -> randomFrom(DateTimeExtractor.values())); | ||
| return new DateTimeProcessor(replaced, UTC); | ||
| return new DateTimeProcessor(replaced, randomValueOtherThan(UTC, ESTestCase::randomZone)); |
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 do you use here random other than UTC?
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.
No reason, I will change.
| } | ||
|
|
||
| @Override | ||
| public Object fold() { |
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.
Completely overriding this method here doesn't feel right. The field folding and the ZonedDateTime result is already handled in BaseDateTimeFunction...
Could this code be integrated in/moved to the BaseDateTimeFunction class and the concrete classes to implement only a doFold method? Maybe using a generic parameter for doFold depending on the class that implements it to be a date-type of class or a time-type of class?
Object doFold(T dateOrTime)
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 decided to do the refactoring mentioned here: #40566 and just delegate to the processor.
costin
left a comment
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.
Left another round of comments.
Since OffsetTime is introduced in scripting, there should be some tests with HAVING against some kind of time component to make sure nothing wack happens.
|
|
||
| private JdbcDateUtils() {} | ||
|
|
||
| // Not available in Java 8 |
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.
Then why use it?
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.
In > Java8 there is already a LocalDate.EPOCH constant, but since we're 8 compatible it cannot be used.
| return floatValue(v); // Float might be represented as string for infinity and NaN values | ||
| case DATE: | ||
| return JdbcDateUtils.asDateTimeField(v, JdbcDateUtils::asDate, Date::new); | ||
| case TIME: |
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.
You could use static imports for JdbcDateUtils.asXXX methods like in the JdbcResultSet
| List<URL> urls = JdbcTestUtils.classpathResources("/*.csv-spec"); | ||
| assertTrue("Not enough specs found " + urls.toString(), urls.size() > 15); | ||
| return readScriptSpec(urls, specParser()); | ||
| Parser parser = specParser(); |
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 remove the classpath scanning?
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.
thx for catching it, I messed up the merge...
| } | ||
|
|
||
|
|
||
| if (value instanceof OffsetTime) { |
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 keep finding the insertion order inconsistent - is it alphabetical or usage based?
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.
Changing it on usage based, but will come up with a followup PR to change it everywhere.
| return MinuteOfDay::new; | ||
| } | ||
|
|
||
| @Override |
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.
Push the method in the parent class to avoid having to repeat it in its children.
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 method was added to TimeFunction but forgot to remove it from the children, thx!
| if (input instanceof OffsetTime) { | ||
| return doProcess(asTimeAtZone((OffsetTime) input, zoneId())); | ||
| } | ||
| if (input instanceof ZonedDateTime) { |
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.
This section could be delegated to the super class which already does ZonedDateTime handling.
| } | ||
|
|
||
| if (l instanceof OffsetTime) { | ||
| r = r.withDays(0).withMonths(0).withYears(0); |
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.
Period contains years/months/days - if everything is zero then Period.ZERO should be returned.
Otherwise Period.of(int, int, int) works.
| } | ||
|
|
||
| return l.plus(r); | ||
| static Temporal add(Temporal l, Period r) { |
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.
Temporal::plus/minus actually delegate to TemporalAmount::addTo/substractFrom which is what Duration and Period are. Considering the latter types are not used anymore, it makes sense to have just one method with Temporal and TemporalAmount and a boolean or enum to indicate whether it's addition or substraction.
It's easier and more accurate vs the closure which is too broad. I would still opt for type checks on either side to make sure no unknown objects are being sent.
|
|
||
| public static final ZoneId UTC = ZoneId.of("Z"); | ||
| public static final String DATE_PARSE_FORMAT = "epoch_millis"; | ||
| // Not available in Java 8 |
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.
Again, what's with this comment? If something is not available how can it be defined?
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.
Changing the comment to // In Java 8 LocalDate.EPOCH is not yet available, as one might wonder why we don't use the predefined constant and will be a marker to remove it when Java8 compatibility is dropped.
|
@elasticmachine run elasticsearch-ci/2 |
|
@elasticmachine run elasticsearch-ci/2 |
costin
left a comment
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
|
|
||
|
|
||
| timeAsHavingFilter | ||
| SELECT MINUTE_OF_HOUR(MAX(birth_date)::TIME + INTERVAL 10 MINUTES) as minute, gender FROM test_emp GROUP BY gender HAVING CAST(MAX(birth_date) AS TIME) = CAST('00:00:00.000' AS TIME) ORDER BY gender; |
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.
👍
| return null; | ||
| } | ||
|
|
||
| if (l instanceof OffsetTime) { |
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.
You could fold the two methods by passing the duration/period as TemporalAmount and then do a type check internally:
if (l instanceof OffsetTime) {
if (r instanceof Period) {
return l;
} else if (r instanceof Duration) {
r = ((Duration) r).ofMillis(..);
} else {
throw new SqlIllegalArgumentException("Unsupported Temporal type [{}]", r);
}it's not too ugly and avoid the null check and if (operation == ) repetition.
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.
Imho since we know if it's a Period or Duration it's "ugly" to merge them into TemporalAmount and then do another instanceof (we do it already for the OffsetTime).
Full format for TIME is `00:00:00.123456789+10:00` so 24 chars long.
astefan
left a comment
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
| private JdbcDateUtils() {} | ||
|
|
||
| // Not available in Java 8 | ||
| // In Java 8 LocalDate.EPOCH is not yet available |
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.
This comment sounds like EPOCH will soon be added to Java 8.
| assertQuery("SELECT CAST(-26853765751000 AS DATE)", "CAST(-26853765751000 AS DATE)", | ||
| "date", "1119-01-15T00:00:00.000Z", 24); | ||
|
|
||
| assertQuery("SELECT CAST('12:29:25.123Z' AS TIME)", "CAST('12:29:25.123Z' AS TIME)", |
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 tests' values don't reflect the maximum precision of the TIME type.
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.
As discussed we don't currently return more than 3 digits (millis), so changed it to 18.
Currently we only display down to millis.
Support ANSI SQL's TIME type by introductin a runtime-only ES SQL time type. Closes: elastic#38174
Support ANSI SQL's TIME type by introducing a runtime-only
ES SQL time type.
Closes: #38174