Skip to content

Commit f233452

Browse files
committed
SQL: protocol returns ISO 8601 String formatted dates instead of Long for JDBC/ODBC requests (#36800)
* Change the way the protocol returns date fields from Long values in case of JDBC/ODBC, to ISO 8601 with millis String. (cherry picked from commit d31eaf7)
1 parent a99e9ca commit f233452

File tree

7 files changed

+118
-39
lines changed

7 files changed

+118
-39
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.jdbc;
8+
9+
import java.sql.Date;
10+
import java.sql.Time;
11+
import java.sql.Timestamp;
12+
import java.time.ZonedDateTime;
13+
import java.time.format.DateTimeFormatter;
14+
import java.time.format.DateTimeFormatterBuilder;
15+
import java.util.Locale;
16+
import java.util.function.Function;
17+
18+
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
19+
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
20+
import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
21+
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
22+
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
23+
24+
/**
25+
* JDBC specific datetime specific utility methods. Because of lack of visibility, this class borrows code
26+
* from {@code org.elasticsearch.xpack.sql.util.DateUtils} and {@code org.elasticsearch.xpack.sql.proto.StringUtils}.
27+
*/
28+
final class JdbcDateUtils {
29+
30+
private static final long DAY_IN_MILLIS = 60 * 60 * 24 * 1000;
31+
32+
static final DateTimeFormatter ISO_WITH_MILLIS = new DateTimeFormatterBuilder()
33+
.parseCaseInsensitive()
34+
.append(ISO_LOCAL_DATE)
35+
.appendLiteral('T')
36+
.appendValue(HOUR_OF_DAY, 2)
37+
.appendLiteral(':')
38+
.appendValue(MINUTE_OF_HOUR, 2)
39+
.appendLiteral(':')
40+
.appendValue(SECOND_OF_MINUTE, 2)
41+
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
42+
.appendOffsetId()
43+
.toFormatter(Locale.ROOT);
44+
45+
static long asMillisSinceEpoch(String date) {
46+
ZonedDateTime zdt = ISO_WITH_MILLIS.parse(date, ZonedDateTime::from);
47+
return zdt.toInstant().toEpochMilli();
48+
}
49+
50+
static Date asDate(String date) {
51+
return new Date(utcMillisRemoveTime(asMillisSinceEpoch(date)));
52+
}
53+
54+
static Time asTime(String date) {
55+
return new Time(utcMillisRemoveDate(asMillisSinceEpoch(date)));
56+
}
57+
58+
static Timestamp asTimestamp(String date) {
59+
return new Timestamp(asMillisSinceEpoch(date));
60+
}
61+
62+
/*
63+
* Handles the value received as parameter, as either String (a ZonedDateTime formatted in ISO 8601 standard with millis) -
64+
* date fields being returned formatted like this. Or a Long value, in case of Histograms.
65+
*/
66+
static <R> R asDateTimeField(Object value, Function<String, R> asDateTimeMethod, Function<Long, R> ctor) {
67+
if (value instanceof String) {
68+
return asDateTimeMethod.apply((String) value);
69+
} else {
70+
return ctor.apply(((Number) value).longValue());
71+
}
72+
}
73+
74+
private static long utcMillisRemoveTime(long l) {
75+
return l - (l % DAY_IN_MILLIS);
76+
}
77+
78+
private static long utcMillisRemoveDate(long l) {
79+
return l % DAY_IN_MILLIS;
80+
}
81+
}

x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/JdbcResultSet.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.util.List;
3131
import java.util.Locale;
3232
import java.util.Map;
33+
import java.util.function.Function;
3334

3435
import static java.lang.String.format;
3536

@@ -248,7 +249,10 @@ private Long dateTime(int columnIndex) throws SQLException {
248249
// the cursor can return an Integer if the date-since-epoch is small enough, XContentParser (Jackson) will
249250
// return the "smallest" data type for numbers when parsing
250251
// TODO: this should probably be handled server side
251-
return val == null ? null : ((Number) val).longValue();
252+
if (val == null) {
253+
return null;
254+
}
255+
return JdbcDateUtils.asDateTimeField(val, JdbcDateUtils::asMillisSinceEpoch, Function.identity());
252256
};
253257
return val == null ? null : (Long) val;
254258
} catch (ClassCastException cce) {

x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/TypeConverter.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ final class TypeConverter {
4848

4949
private TypeConverter() {}
5050

51-
private static final long DAY_IN_MILLIS = 60 * 60 * 24 * 1000;
52-
5351
/**
5452
* Converts millisecond after epoc to date
5553
*/
@@ -216,7 +214,7 @@ static Object convert(Object v, EsType columnType, String typeString) throws SQL
216214
case FLOAT:
217215
return floatValue(v); // Float might be represented as string for infinity and NaN values
218216
case DATE:
219-
return new Timestamp(((Number) v).longValue());
217+
return JdbcDateUtils.asDateTimeField(v, JdbcDateUtils::asTimestamp, Timestamp::new);
220218
case INTERVAL_YEAR:
221219
case INTERVAL_MONTH:
222220
case INTERVAL_YEAR_TO_MONTH:
@@ -470,21 +468,21 @@ private static Double asDouble(Object val, EsType columnType, String typeString)
470468

471469
private static Date asDate(Object val, EsType columnType, String typeString) throws SQLException {
472470
if (columnType == EsType.DATE) {
473-
return new Date(utcMillisRemoveTime(((Number) val).longValue()));
471+
return JdbcDateUtils.asDateTimeField(val, JdbcDateUtils::asDate, Date::new);
474472
}
475473
return failConversion(val, columnType, typeString, Date.class);
476474
}
477475

478476
private static Time asTime(Object val, EsType columnType, String typeString) throws SQLException {
479477
if (columnType == EsType.DATE) {
480-
return new Time(utcMillisRemoveDate(((Number) val).longValue()));
478+
return JdbcDateUtils.asDateTimeField(val, JdbcDateUtils::asTime, Time::new);
481479
}
482480
return failConversion(val, columnType, typeString, Time.class);
483481
}
484482

485483
private static Timestamp asTimestamp(Object val, EsType columnType, String typeString) throws SQLException {
486484
if (columnType == EsType.DATE) {
487-
return new Timestamp(((Number) val).longValue());
485+
return JdbcDateUtils.asDateTimeField(val, JdbcDateUtils::asTimestamp, Timestamp::new);
488486
}
489487
return failConversion(val, columnType, typeString, Timestamp.class);
490488
}
@@ -513,14 +511,6 @@ private static OffsetDateTime asOffsetDateTime(Object val, EsType columnType, St
513511
throw new SQLFeatureNotSupportedException();
514512
}
515513

516-
private static long utcMillisRemoveTime(long l) {
517-
return l - (l % DAY_IN_MILLIS);
518-
}
519-
520-
private static long utcMillisRemoveDate(long l) {
521-
return l % DAY_IN_MILLIS;
522-
}
523-
524514
private static byte safeToByte(long x) throws SQLException {
525515
if (x > Byte.MAX_VALUE || x < Byte.MIN_VALUE) {
526516
throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", Long.toString(x)));

x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/JdbcIntegrationTestCase.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import static org.elasticsearch.xpack.sql.qa.rest.RestSqlTestCase.assertNoSearchContexts;
3131

3232
public abstract class JdbcIntegrationTestCase extends ESRestTestCase {
33+
3334
@After
3435
public void checkSearchContent() throws Exception {
3536
// Some context might linger due to fire and forget nature of scroll cleanup

x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/JdbcTestUtils.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import java.sql.ResultSet;
1313
import java.sql.ResultSetMetaData;
1414
import java.sql.SQLException;
15+
import java.time.Instant;
16+
import java.time.ZoneId;
17+
import java.time.ZonedDateTime;
1518
import java.util.ArrayList;
1619
import java.util.List;
1720

@@ -20,6 +23,8 @@ public abstract class JdbcTestUtils {
2023
public static final String SQL_TRACE = "org.elasticsearch.xpack.sql:TRACE";
2124

2225
public static final String JDBC_TIMEZONE = "timezone";
26+
27+
public static ZoneId UTC = ZoneId.of("Z");
2328

2429
public static void logResultSetMetadata(ResultSet rs, Logger logger) throws SQLException {
2530
ResultSetMetaData metaData = rs.getMetaData();
@@ -128,4 +133,8 @@ public static void logLikeCLI(ResultSet rs, Logger logger) throws SQLException {
128133
CliFormatter formatter = new CliFormatter(cols, data);
129134
logger.info("\n" + formatter.formatWithHeader(cols, data));
130135
}
136+
137+
public static ZonedDateTime of(long millis) {
138+
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), UTC);
139+
}
131140
}

x-pack/plugin/sql/qa/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import static java.util.Calendar.SECOND;
5959
import static java.util.Calendar.YEAR;
6060
import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.JDBC_TIMEZONE;
61+
import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.of;
6162

6263
public class ResultSetTestCase extends JdbcIntegrationTestCase {
6364

@@ -200,10 +201,10 @@ public void testGettingInvalidByte() throws Exception {
200201
sqle.getMessage());
201202

202203
sqle = expectThrows(SQLException.class, () -> results.getByte("test_date"));
203-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Byte]", randomDate),
204+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Byte]", of(randomDate)),
204205
sqle.getMessage());
205206
sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Byte.class));
206-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Byte]", randomDate),
207+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Byte]", of(randomDate)),
207208
sqle.getMessage());
208209
});
209210
}
@@ -323,10 +324,10 @@ public void testGettingInvalidShort() throws Exception {
323324
sqle.getMessage());
324325

325326
sqle = expectThrows(SQLException.class, () -> results.getShort("test_date"));
326-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Short]", randomDate),
327+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Short]", of(randomDate)),
327328
sqle.getMessage());
328329
sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Short.class));
329-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Short]", randomDate),
330+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Short]", of(randomDate)),
330331
sqle.getMessage());
331332
});
332333
}
@@ -438,10 +439,10 @@ public void testGettingInvalidInteger() throws Exception {
438439
sqle.getMessage());
439440

440441
sqle = expectThrows(SQLException.class, () -> results.getInt("test_date"));
441-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Integer]", randomDate),
442+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Integer]", of(randomDate)),
442443
sqle.getMessage());
443444
sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Integer.class));
444-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Integer]", randomDate),
445+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Integer]", of(randomDate)),
445446
sqle.getMessage());
446447
});
447448
}
@@ -540,10 +541,10 @@ public void testGettingInvalidLong() throws Exception {
540541
sqle.getMessage());
541542

542543
sqle = expectThrows(SQLException.class, () -> results.getLong("test_date"));
543-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Long]", randomDate),
544+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Long]", of(randomDate)),
544545
sqle.getMessage());
545546
sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Long.class));
546-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Long]", randomDate),
547+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Long]", of(randomDate)),
547548
sqle.getMessage());
548549
});
549550
}
@@ -623,10 +624,10 @@ public void testGettingInvalidDouble() throws Exception {
623624
sqle.getMessage());
624625

625626
sqle = expectThrows(SQLException.class, () -> results.getDouble("test_date"));
626-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Double]", randomDate),
627+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Double]", of(randomDate)),
627628
sqle.getMessage());
628629
sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Double.class));
629-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Double]", randomDate),
630+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Double]", of(randomDate)),
630631
sqle.getMessage());
631632
});
632633
}
@@ -706,10 +707,10 @@ public void testGettingInvalidFloat() throws Exception {
706707
sqle.getMessage());
707708

708709
sqle = expectThrows(SQLException.class, () -> results.getFloat("test_date"));
709-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Float]", randomDate),
710+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Float]", of(randomDate)),
710711
sqle.getMessage());
711712
sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Float.class));
712-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Float]", randomDate),
713+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Float]", of(randomDate)),
713714
sqle.getMessage());
714715
});
715716
}
@@ -767,7 +768,7 @@ public void testGettingBooleanValues() throws Exception {
767768
assertEquals("Expected: <true> but was: <false> for field " + fld, true, results.getObject(fld, Boolean.class));
768769
}
769770
SQLException sqle = expectThrows(SQLException.class, () -> results.getBoolean("test_date"));
770-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Boolean]", randomDate1),
771+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Boolean]", of(randomDate1)),
771772
sqle.getMessage());
772773

773774
results.next();
@@ -777,11 +778,11 @@ public void testGettingBooleanValues() throws Exception {
777778
assertEquals("Expected: <false> but was: <true> for field " + fld, false, results.getObject(fld, Boolean.class));
778779
}
779780
sqle = expectThrows(SQLException.class, () -> results.getBoolean("test_date"));
780-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Boolean]", randomDate2),
781+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Boolean]", of(randomDate2)),
781782
sqle.getMessage());
782783

783784
sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Boolean.class));
784-
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Boolean]", randomDate2),
785+
assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [DATE] to [Boolean]", of(randomDate2)),
785786
sqle.getMessage());
786787

787788
results.next();

x-pack/plugin/sql/sql-action/src/main/java/org/elasticsearch/xpack/sql/action/SqlQueryResponse.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,15 +171,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
171171
public static XContentBuilder value(XContentBuilder builder, Mode mode, Object value) throws IOException {
172172
if (value instanceof ZonedDateTime) {
173173
ZonedDateTime zdt = (ZonedDateTime) value;
174-
if (Mode.isDriver(mode)) {
175-
// JDBC cannot parse dates in string format and ODBC can have issues with it
176-
// so instead, use the millis since epoch (in UTC)
177-
builder.value(zdt.toInstant().toEpochMilli());
178-
}
179-
// otherwise use the ISO format
180-
else {
181-
builder.value(StringUtils.toString(zdt));
182-
}
174+
// use the ISO format
175+
builder.value(StringUtils.toString(zdt));
183176
} else {
184177
builder.value(value);
185178
}

0 commit comments

Comments
 (0)