Skip to content

Conversation

@matriv
Copy link
Contributor

@matriv matriv commented Mar 7, 2019

Support ANSI SQL's TIME type by introducing a runtime-only
ES SQL time type.

Closes: #38174

@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-search

Support ANSI SQL's TIME type by introductin a runtime-only
ES SQL time type.

Closes: elastic#38174
Copy link
Member

@costin costin 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 working on this.
I've left a first round of comments.

1955-01-21 00:00:00Z | Maliniak
;

timeAsFilter_NoMatch
Copy link
Member

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) {
Copy link
Member

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));
Copy link
Member

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).

Copy link
Contributor Author

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.

Copy link
Contributor Author

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) {
Copy link
Member

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())),
Copy link
Member

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()
Copy link
Member

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...

Copy link
Contributor Author

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) {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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);
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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":
Copy link
Member

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.

Copy link
Contributor

@astefan astefan left a 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()
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.appendOffsetId()
.toFormatter(Locale.ROOT);

private static final DateTimeFormatter ISO_WITH_MILLIS = new DateTimeFormatterBuilder()
Copy link
Contributor

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

isTemporal sounds better?

Copy link
Contributor Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

isNumericOrTemporal sounds better?

Copy link
Contributor Author

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())) {
Copy link
Contributor

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() {
Copy link
Contributor

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@matriv matriv requested review from astefan and costin March 28, 2019 01:28
@matriv
Copy link
Contributor Author

matriv commented Mar 28, 2019

@costin @astefan
Thank you very much for the comments.
Since the PR is already pretty big I will followup with 2 more:

  • A PR that changes the data set to add some times (currently birth_date & hire_date are always 00:00:00.000 and adds more integ tests
  • A PR that changes the order of code blocks to DATETIME, DATE, TIME in order of priority

@matriv
Copy link
Contributor Author

matriv commented Mar 28, 2019

@elasticmachine run elasticsearch-ci/2

Copy link
Contributor

@astefan astefan left a 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));
Copy link
Contributor

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?

Copy link
Contributor Author

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() {
Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Member

@costin costin left a 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
Copy link
Member

Choose a reason for hiding this comment

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

Then why use it?

Copy link
Contributor Author

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:
Copy link
Member

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();
Copy link
Member

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?

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 for catching it, I messed up the merge...

}


if (value instanceof OffsetTime) {
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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);
Copy link
Member

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) {
Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

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.

@matriv matriv requested review from astefan and costin and removed request for costin March 29, 2019 16:04
@matriv
Copy link
Contributor Author

matriv commented Mar 29, 2019

@costin @astefan As discussed, grouping by TIME is currently disabled as it requires more work to implement regarding painless scripting. Tracked here: #40639

@matriv
Copy link
Contributor Author

matriv commented Mar 29, 2019

@elasticmachine run elasticsearch-ci/2

@matriv
Copy link
Contributor Author

matriv commented Mar 29, 2019

@elasticmachine run elasticsearch-ci/2

Copy link
Member

@costin costin left a 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;
Copy link
Member

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) {
Copy link
Member

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.

Copy link
Contributor Author

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).

matriv and others added 3 commits March 29, 2019 23:32
Copy link
Contributor

@astefan astefan left a 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
Copy link
Contributor

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)",
Copy link
Contributor

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.

Copy link
Contributor Author

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.

matriv added 2 commits April 1, 2019 17:53
Currently we only display down to millis.
@matriv
Copy link
Contributor Author

matriv commented Apr 1, 2019

@costin @astefan Many thx for the repeated reviews!

@matriv matriv merged commit 046ccd4 into elastic:master Apr 1, 2019
@matriv matriv deleted the mt/impl-38174 branch April 1, 2019 21:30
matriv added a commit that referenced this pull request Apr 1, 2019
Support ANSI SQL's TIME type by introductin a runtime-only
ES SQL time type.

Closes: #38174
(cherry picked from commit 046ccd4)
gurkankaymak pushed a commit to gurkankaymak/elasticsearch that referenced this pull request May 27, 2019
Support ANSI SQL's TIME type by introductin a runtime-only
ES SQL time type.

Closes: elastic#38174
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL: Introduce SQL TIME type

5 participants