SQL: Relax parsing of date/time escaped literals (#58336) (#58450)

Improve the usability of the MS-SQL server/ODBC escaped
date/time/timestamp literals, by allowing timezone/offset ids
in the parsed string, e.g.:
```
{ts '2000-01-01T11:11:11Z'}
```

Closes: #58262
(cherry picked from commit 0af1f2fef805324e802d97d2fd9b4660abb403f0)
This commit is contained in:
Marios Trivyzas 2020-06-23 18:05:54 +02:00 committed by GitHub
parent 642b05a511
commit e7c40d973e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 45 additions and 64 deletions

View File

@ -133,8 +133,8 @@ import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.elasticsearch.xpack.sql.type.SqlDataTypeConverter.canConvert;
import static org.elasticsearch.xpack.sql.type.SqlDataTypeConverter.converterFor;
import static org.elasticsearch.xpack.sql.util.DateUtils.asDateOnly;
import static org.elasticsearch.xpack.sql.util.DateUtils.asTimeOnly;
import static org.elasticsearch.xpack.sql.util.DateUtils.dateOfEscapedLiteral;
import static org.elasticsearch.xpack.sql.util.DateUtils.dateTimeOfEscapedLiteral;
abstract class ExpressionBuilder extends IdentifierBuilder {
@ -778,7 +778,7 @@ abstract class ExpressionBuilder extends IdentifierBuilder {
Source source = source(ctx);
// parse yyyy-MM-dd (time optional but is set to 00:00:00.000 because of the conversion to DATE
try {
return new Literal(source, dateOfEscapedLiteral(string), SqlDataTypes.DATE);
return new Literal(source, asDateOnly(string), SqlDataTypes.DATE);
} catch(DateTimeParseException ex) {
throw new ParsingException(source, "Invalid date received; {}", ex.getMessage());
}

View File

@ -28,7 +28,6 @@ import java.time.temporal.TemporalAccessor;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
import static java.time.format.DateTimeFormatter.ISO_TIME;
public final class DateUtils {
@ -37,38 +36,23 @@ public final class DateUtils {
public static final LocalDate EPOCH = LocalDate.of(1970, 1, 1);
public static final long DAY_IN_MILLIS = 60 * 60 * 24 * 1000L;
private static final DateTimeFormatter DATE_TIME_FORMATTER_WHITESPACE = new DateTimeFormatterBuilder()
.append(ISO_LOCAL_DATE)
.appendLiteral(' ')
private static final DateTimeFormatter ISO_LOCAL_TIME_OPTIONAL_TZ = new DateTimeFormatterBuilder()
.append(ISO_LOCAL_TIME)
.toFormatter().withZone(UTC);
private static final DateTimeFormatter DATE_TIME_FORMATTER_T_LITERAL = new DateTimeFormatterBuilder()
.append(ISO_LOCAL_DATE)
.appendLiteral('T')
.append(ISO_LOCAL_TIME)
.toFormatter().withZone(UTC);
private static final DateTimeFormatter DATE_OPTIONAL_TIME_FORMATTER_WHITESPACE = new DateTimeFormatterBuilder()
.append(ISO_LOCAL_DATE)
.optionalStart()
.appendLiteral(' ')
.append(ISO_LOCAL_TIME)
.toFormatter().withZone(UTC);
private static final DateTimeFormatter DATE_OPTIONAL_TIME_FORMATTER_T_LITERAL = new DateTimeFormatterBuilder()
.append(ISO_LOCAL_DATE)
.optionalStart()
.appendLiteral('T')
.append(ISO_LOCAL_TIME)
.toFormatter().withZone(UTC);
private static final DateTimeFormatter ISO_LOCAL_DATE_OPTIONAL_TIME_FORMATTER_WHITESPACE = new DateTimeFormatterBuilder()
.append(DATE_OPTIONAL_TIME_FORMATTER_WHITESPACE)
.optionalStart()
.appendZoneOrOffsetId()
.toFormatter().withZone(UTC);
private static final DateTimeFormatter ISO_LOCAL_DATE_OPTIONAL_TIME_FORMATTER_WHITESPACE = new DateTimeFormatterBuilder()
.append(ISO_LOCAL_DATE)
.optionalStart()
.appendLiteral(' ')
.append(ISO_LOCAL_TIME_OPTIONAL_TZ)
.optionalEnd()
.toFormatter().withZone(UTC);
private static final DateTimeFormatter ISO_LOCAL_DATE_OPTIONAL_TIME_FORMATTER_T_LITERAL = new DateTimeFormatterBuilder()
.append(DATE_OPTIONAL_TIME_FORMATTER_T_LITERAL)
.append(ISO_LOCAL_DATE)
.optionalStart()
.appendZoneOrOffsetId()
.appendLiteral('T')
.append(ISO_LOCAL_TIME_OPTIONAL_TZ)
.optionalEnd()
.toFormatter().withZone(UTC);
@ -120,15 +104,7 @@ public final class DateUtils {
* Parses the given string into a Date (SQL DATE type) using UTC as a default timezone.
*/
public static ZonedDateTime asDateOnly(String dateFormat) {
int separatorIdx = dateFormat.indexOf('-'); // Find the first `-` date separator
if (separatorIdx == 0) { // first char = `-` denotes a negative year
separatorIdx = dateFormat.indexOf('-', 1); // Find the first `-` date separator past the negative year
}
// Find the second `-` date separator and move 3 places past the dayOfYear to find the time separator
// e.g. 2020-06-01T10:20:30....
// ^
// +3 = ^
separatorIdx = dateFormat.indexOf('-', separatorIdx + 1) + 3;
int separatorIdx = timeSeparatorIdx(dateFormat);
// Avoid index out of bounds - it will lead to DateTimeParseException anyways
if (separatorIdx >= dateFormat.length() || dateFormat.charAt(separatorIdx) == 'T') {
return LocalDate.parse(dateFormat, ISO_LOCAL_DATE_OPTIONAL_TIME_FORMATTER_T_LITERAL).atStartOfDay(UTC);
@ -142,7 +118,7 @@ public final class DateUtils {
}
public static OffsetTime asTimeOnly(String timeFormat) {
return DateFormatters.from(ISO_TIME.parse(timeFormat)).toOffsetDateTime().toOffsetTime();
return DateFormatters.from(ISO_LOCAL_TIME_OPTIONAL_TZ.parse(timeFormat)).toOffsetDateTime().toOffsetTime();
}
/**
@ -152,23 +128,13 @@ public final class DateUtils {
return DateFormatters.from(UTC_DATE_TIME_FORMATTER.parse(dateFormat)).withZoneSameInstant(UTC);
}
public static ZonedDateTime dateOfEscapedLiteral(String dateFormat) {
int separatorIdx = dateFormat.lastIndexOf('-') + 3;
// Avoid index out of bounds - it will lead to DateTimeParseException anyways
if (separatorIdx >= dateFormat.length() || dateFormat.charAt(separatorIdx) == 'T') {
return LocalDate.parse(dateFormat, DATE_OPTIONAL_TIME_FORMATTER_T_LITERAL).atStartOfDay(UTC);
} else {
return LocalDate.parse(dateFormat, DATE_TIME_FORMATTER_WHITESPACE).atStartOfDay(UTC);
}
}
public static ZonedDateTime dateTimeOfEscapedLiteral(String dateFormat) {
int separatorIdx = dateFormat.lastIndexOf('-') + 3;
int separatorIdx = timeSeparatorIdx(dateFormat);
// Avoid index out of bounds - it will lead to DateTimeParseException anyways
if (separatorIdx >= dateFormat.length() || dateFormat.charAt(separatorIdx) == 'T') {
return ZonedDateTime.parse(dateFormat, DATE_TIME_FORMATTER_T_LITERAL.withZone(UTC));
return ZonedDateTime.parse(dateFormat, ISO_LOCAL_DATE_OPTIONAL_TIME_FORMATTER_T_LITERAL);
} else {
return ZonedDateTime.parse(dateFormat, DATE_TIME_FORMATTER_WHITESPACE.withZone(UTC));
return ZonedDateTime.parse(dateFormat, ISO_LOCAL_DATE_OPTIONAL_TIME_FORMATTER_WHITESPACE);
}
}
@ -243,4 +209,16 @@ public final class DateUtils {
return ta;
}
}
private static int timeSeparatorIdx(String timestampStr) {
int separatorIdx = timestampStr.indexOf('-'); // Find the first `-` date separator
if (separatorIdx == 0) { // first char = `-` denotes a negative year
separatorIdx = timestampStr.indexOf('-', 1); // Find the first `-` date separator past the negative year
}
// Find the second `-` date separator and move 3 places past the dayOfYear to find the time separator
// e.g. 2020-06-01T10:20:30....
// ^
// +3 = ^
return timestampStr.indexOf('-', separatorIdx + 1) + 3;
}
}

View File

@ -34,6 +34,7 @@ import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.startsWith;
public class EscapedFunctionsTests extends ESTestCase {
@ -83,17 +84,19 @@ public class EscapedFunctionsTests extends ESTestCase {
private String buildTime() {
if (randomBoolean()) {
return (randomBoolean() ? "T" : " ") + "11:22" + buildSecsAndFractional();
return (randomBoolean() ? "T" : " ") + "11:22" + buildSecsFractionalAndTimezone();
}
return "";
}
private String buildSecsAndFractional() {
private String buildSecsFractionalAndTimezone() {
String str = "";
if (randomBoolean()) {
return ":55" + randomFrom("", ".1", ".12", ".123", ".1234", ".12345", ".123456",
".1234567", ".12345678", ".123456789");
str = ":55" + randomFrom("", ".1", ".12", ".123", ".1234", ".12345", ".123456",
".1234567", ".12345678", ".123456789") +
randomFrom("", "Z", "Etc/GMT-5", "-05:30", "+04:20");
}
return "";
return str;
}
private Literal guidLiteral(String guid) {
@ -231,7 +234,7 @@ public class EscapedFunctionsTests extends ESTestCase {
}
public void testTimeLiteral() {
Literal l = timeLiteral("12:23" + buildSecsAndFractional());
Literal l = timeLiteral("12:23" + buildSecsFractionalAndTimezone());
assertThat(l.dataType(), is(TIME));
}
@ -243,9 +246,9 @@ public class EscapedFunctionsTests extends ESTestCase {
}
public void testTimestampLiteral() {
Literal l = timestampLiteral(buildDate() + " 10:20" + buildSecsAndFractional());
Literal l = timestampLiteral(buildDate() + " 10:20" + buildSecsFractionalAndTimezone());
assertThat(l.dataType(), is(DATETIME));
l = timestampLiteral(buildDate() + "T11:22" + buildSecsAndFractional());
l = timestampLiteral(buildDate() + "T11:22" + buildSecsFractionalAndTimezone());
assertThat(l.dataType(), is(DATETIME));
}
@ -253,8 +256,8 @@ public class EscapedFunctionsTests extends ESTestCase {
String date = buildDate();
ParsingException ex = expectThrows(ParsingException.class, () -> timestampLiteral(date+ "_AB 10:01:02.3456"));
assertEquals(
"line 1:2: Invalid timestamp received; Text '" + date + "_AB 10:01:02.3456' could not be parsed at index " +
date.length(),
"line 1:2: Invalid timestamp received; Text '" + date + "_AB 10:01:02.3456' could not be parsed, " +
"unparsed text found at index " + date.length(),
ex.getMessage());
ex = expectThrows(ParsingException.class, () -> timestampLiteral("20120101_AB 10:01:02.3456"));
assertEquals(
@ -262,9 +265,9 @@ public class EscapedFunctionsTests extends ESTestCase {
ex.getMessage());
ex = expectThrows(ParsingException.class, () -> timestampLiteral(date));
assertEquals(
"line 1:2: Invalid timestamp received; Text '" + date + "' could not be parsed at index " + date.length(),
ex.getMessage());
assertThat(ex.getMessage(), startsWith(
"line 1:2: Invalid timestamp received; Text '" + date + "' could not be parsed: " +
"Unable to obtain ZonedDateTime from TemporalAccessor"));
}
public void testGUID() {