Skip to content

Commit b75d2bc

Browse files
Complete fix and update tests.
Signed-off-by: Yury-Fridlyand <[email protected]>
1 parent 5c22386 commit b75d2bc

File tree

5 files changed

+273
-232
lines changed

5 files changed

+273
-232
lines changed

integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFormatsIT.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,18 @@ public void testCustomFormats2() {
9292
@Test
9393
@SneakyThrows
9494
public void testIncompleteFormats() {
95-
String query = String.format("SELECT incomplete_1, incomplete_2, incorrect"
96-
+ " FROM %s", TEST_INDEX_DATE_FORMATS);
95+
String query = String.format("SELECT incomplete_1, incomplete_2, incorrect,"
96+
+ "incomplete_custom_time, incomplete_custom_date FROM %s", TEST_INDEX_DATE_FORMATS);
9797
JSONObject result = executeQuery(query);
9898
verifySchema(result,
9999
schema("incomplete_1", null, "timestamp"),
100-
schema("incomplete_2", null, "timestamp"),
101-
schema("incorrect", null, "timestamp"));
100+
schema("incomplete_2", null, "date"),
101+
schema("incorrect", null, "timestamp"),
102+
schema("incomplete_custom_time", null, "time"),
103+
schema("incomplete_custom_date", null, "date"));
102104
verifyDataRows(result,
103-
rows("1984-01-01 00:00:00", null, null),
104-
rows("2012-01-01 00:00:00", null, null));
105+
rows("1984-01-01 00:00:00", null, null, "10:00:00", "1999-01-01"),
106+
rows("2012-01-01 00:00:00", null, null, "20:00:00", "3021-01-01"));
105107
}
106108

107109
@Test

opensearch/src/main/java/org/opensearch/sql/opensearch/data/type/OpenSearchDateType.java

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@
1111
import static org.opensearch.sql.data.type.ExprCoreType.TIME;
1212
import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP;
1313

14-
import java.time.LocalDateTime;
15-
import java.time.temporal.TemporalAccessor;
1614
import java.util.List;
1715
import java.util.Objects;
1816
import java.util.stream.Collectors;
1917
import lombok.EqualsAndHashCode;
2018
import org.opensearch.common.time.DateFormatter;
21-
import org.opensearch.common.time.DateFormatters;
2219
import org.opensearch.common.time.FormatNames;
2320
import org.opensearch.sql.data.type.ExprCoreType;
2421
import org.opensearch.sql.data.type.ExprType;
@@ -31,13 +28,13 @@ public class OpenSearchDateType extends OpenSearchDataType {
3128

3229
private static final OpenSearchDateType instance = new OpenSearchDateType();
3330

34-
// numeric formats which support full datetime
31+
/** Numeric formats which support full datetime. */
3532
public static final List<FormatNames> SUPPORTED_NAMED_NUMERIC_FORMATS = List.of(
3633
FormatNames.EPOCH_MILLIS,
3734
FormatNames.EPOCH_SECOND
3835
);
3936

40-
// list of named formats which support full datetime
37+
/** List of named formats which support full datetime. */
4138
public static final List<FormatNames> SUPPORTED_NAMED_DATETIME_FORMATS = List.of(
4239
FormatNames.ISO8601,
4340
FormatNames.BASIC_DATE_TIME,
@@ -78,7 +75,7 @@ public class OpenSearchDateType extends OpenSearchDataType {
7875
FormatNames.STRICT_WEEK_DATE_TIME_NO_MILLIS
7976
);
8077

81-
// list of named formats that only support year/month/day
78+
/** List of named formats that only support year/month/day. */
8279
public static final List<FormatNames> SUPPORTED_NAMED_DATE_FORMATS = List.of(
8380
FormatNames.BASIC_DATE,
8481
FormatNames.BASIC_ORDINAL_DATE,
@@ -94,8 +91,8 @@ public class OpenSearchDateType extends OpenSearchDataType {
9491
FormatNames.STRICT_WEEKYEAR_WEEK_DAY
9592
);
9693

97-
// list of named formats which produce incomplete date,
98-
// e.g. 1 or 2 are missing from tuple year/month/day
94+
/** list of named formats which produce incomplete date,
95+
* e.g. 1 or 2 are missing from tuple year/month/day. */
9996
public static final List<FormatNames> SUPPORTED_NAMED_INCOMPLETE_DATE_FORMATS = List.of(
10097
FormatNames.YEAR_MONTH,
10198
FormatNames.STRICT_YEAR_MONTH,
@@ -108,7 +105,7 @@ public class OpenSearchDateType extends OpenSearchDataType {
108105
FormatNames.STRICT_WEEKYEAR
109106
);
110107

111-
// list of named formats that only support hour/minute/second
108+
/** List of named formats that only support hour/minute/second. */
112109
public static final List<FormatNames> SUPPORTED_NAMED_TIME_FORMATS = List.of(
113110
FormatNames.BASIC_TIME,
114111
FormatNames.BASIC_TIME_NO_MILLIS,
@@ -134,6 +131,11 @@ public class OpenSearchDateType extends OpenSearchDataType {
134131
FormatNames.STRICT_T_TIME_NO_MILLIS
135132
);
136133

134+
/** Formatter symbols which used to format time or date correspondingly.
135+
* {@link java.time.format.DateTimeFormatter}. */
136+
private static final String CUSTOM_FORMAT_TIME_SYMBOLS = "nNASsmHkKha";
137+
private static final String CUSTOM_FORMAT_DATE_SYMBOLS = "FecEWwYqQgdMLDyuG";
138+
137139
@EqualsAndHashCode.Exclude
138140
String formatString;
139141

@@ -179,7 +181,6 @@ public List<DateFormatter> getAllNamedFormatters() {
179181

180182
/**
181183
* Retrieves a list of numeric formatters that format for dates.
182-
*
183184
* @return a list of DateFormatters that can be used to parse a Date.
184185
*/
185186
public List<DateFormatter> getNumericNamedFormatters() {
@@ -191,6 +192,26 @@ public List<DateFormatter> getNumericNamedFormatters() {
191192
.map(DateFormatter::forPattern).collect(Collectors.toList());
192193
}
193194

195+
/**
196+
* Retrieves a list of custom formats defined by the user.
197+
* @return a list of formats as strings that can be used to parse a Date/Time/Timestamp.
198+
*/
199+
public List<String> getAllCustomFormats() {
200+
return getFormatList().stream()
201+
.filter(format -> FormatNames.forName(format) == null)
202+
.map(format -> {
203+
try {
204+
DateFormatter.forPattern(format);
205+
return format;
206+
} catch (Exception ignored) {
207+
// parsing failed
208+
return null;
209+
}
210+
})
211+
.filter(Objects::nonNull)
212+
.collect(Collectors.toList());
213+
}
214+
194215
/**
195216
* Retrieves a list of custom formatters defined by the user.
196217
* @return a list of DateFormatters that can be used to parse a Date/Time/Timestamp.
@@ -212,7 +233,6 @@ public List<DateFormatter> getAllCustomFormatters() {
212233

213234
/**
214235
* Retrieves a list of named formatters that format for dates.
215-
*
216236
* @return a list of DateFormatters that can be used to parse a Date.
217237
*/
218238
public List<DateFormatter> getDateNamedFormatters() {
@@ -226,7 +246,6 @@ public List<DateFormatter> getDateNamedFormatters() {
226246

227247
/**
228248
* Retrieves a list of named formatters that format for Times.
229-
*
230249
* @return a list of DateFormatters that can be used to parse a Time.
231250
*/
232251
public List<DateFormatter> getTimeNamedFormatters() {
@@ -240,7 +259,6 @@ public List<DateFormatter> getTimeNamedFormatters() {
240259

241260
/**
242261
* Retrieves a list of named formatters that format for DateTimes.
243-
*
244262
* @return a list of DateFormatters that can be used to parse a DateTime.
245263
*/
246264
public List<DateFormatter> getDateTimeNamedFormatters() {
@@ -252,39 +270,26 @@ public List<DateFormatter> getDateTimeNamedFormatters() {
252270
.map(DateFormatter::forPattern).collect(Collectors.toList());
253271
}
254272

255-
private ExprCoreType getExprTypeFromCustomFormats(List<DateFormatter> formats) {
273+
private ExprCoreType getExprTypeFromCustomFormats(List<String> formats) {
256274
boolean isDate = false;
257275
boolean isTime = false;
258276

259-
LocalDateTime sampleDateTime = LocalDateTime.now();
260-
// Unfortunately, there is no public API to get info from the formatter object,
261-
// whether it parses date or time or datetime. The workaround is:
262-
// Converting a sample DateTime object by each formatter to string and back.
263-
// Double-converted sample will be also DateTime, but if formatter parses
264-
// time part only, date part would be lost. And vice versa.
265-
// This trick allows us to get matching data type for every custom formatter.
266-
// Unfortunately, this involves parsing a string, once per query, per column, per format.
267-
// Overhead performance is equal to parsing extra row of result set (extra doc).
268-
// Could be cached in scope of #1783 https://github.com/opensearch-project/sql/issues/1783.
269-
270277
for (var format : formats) {
271-
TemporalAccessor ta = format.parse(format.format(sampleDateTime));
272-
LocalDateTime parsedSample = sampleDateTime;
273-
try {
274-
// TODO do we need withZoneSameInstant or withZoneSameLocal?
275-
parsedSample = DateFormatters.from(ta).toLocalDateTime();
276-
} catch (Exception ignored) {
277-
// Can't convert to a DateTime - format does not represent a complete date or time
278-
continue;
278+
if (!isTime) {
279+
for (var symbol : CUSTOM_FORMAT_TIME_SYMBOLS.toCharArray()) {
280+
if (format.contains(String.valueOf(symbol))) {
281+
isTime = true;
282+
break;
283+
}
284+
}
279285
}
280-
281286
if (!isDate) {
282-
isDate = parsedSample.toLocalDate().equals(sampleDateTime.toLocalDate());
283-
}
284-
if (!isTime) {
285-
// Second and Second fraction part are optional and may miss in some formats, trim it.
286-
isTime = parsedSample.toLocalTime().withSecond(0).withNano(0).equals(
287-
sampleDateTime.toLocalTime().withSecond(0).withNano(0));
287+
for (var symbol : CUSTOM_FORMAT_DATE_SYMBOLS.toCharArray()) {
288+
if (format.contains(String.valueOf(symbol))) {
289+
isDate = true;
290+
break;
291+
}
292+
}
288293
}
289294
if (isDate && isTime) {
290295
return TIMESTAMP;
@@ -298,7 +303,7 @@ private ExprCoreType getExprTypeFromCustomFormats(List<DateFormatter> formats) {
298303
return TIME;
299304
}
300305

301-
// Incomplete formats: can't be converted to DATE nor TIME, for example `year` (year only)
306+
// Incomplete or incorrect formats: can't be converted to DATE nor TIME, for example `year`
302307
return TIMESTAMP;
303308
}
304309

@@ -316,7 +321,7 @@ private ExprCoreType getExprTypeFromFormatString(String formatString) {
316321
return TIMESTAMP;
317322
}
318323

319-
List<DateFormatter> customFormatters = getAllCustomFormatters();
324+
List<String> customFormatters = getAllCustomFormats();
320325
if (!customFormatters.isEmpty()) {
321326
ExprCoreType customFormatType = getExprTypeFromCustomFormats(customFormatters);
322327
ExprCoreType combinedByDefaultFormats = customFormatType;

opensearch/src/main/java/org/opensearch/sql/opensearch/data/value/OpenSearchExprValueFactory.java

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.opensearch.sql.data.model.ExprTimestampValue;
5656
import org.opensearch.sql.data.model.ExprTupleValue;
5757
import org.opensearch.sql.data.model.ExprValue;
58+
import org.opensearch.sql.data.type.ExprCoreType;
5859
import org.opensearch.sql.data.type.ExprType;
5960
import org.opensearch.sql.opensearch.data.type.OpenSearchDataType;
6061
import org.opensearch.sql.opensearch.data.type.OpenSearchDateType;
@@ -226,8 +227,7 @@ private ExprValue parseTimestampString(String value, OpenSearchDateType dateType
226227
try {
227228
parsed = DateFormatters.from(DATE_TIME_FORMATTER.parse(value)).toInstant();
228229
return new ExprTimestampValue(parsed);
229-
} catch (DateTimeParseException e) {
230-
// ignored
230+
} catch (DateTimeParseException ignored) {
231231
}
232232

233233
// otherwise, throw an error that no formatters worked
@@ -254,7 +254,7 @@ private ExprValue parseTimeString(String value, OpenSearchDateType dateType) {
254254
ZonedDateTime zonedDateTime = DateFormatters.from(accessor);
255255
return new ExprTimeValue(
256256
zonedDateTime.withZoneSameLocal(UTC_ZONE_ID).toLocalTime());
257-
} catch (IllegalArgumentException ignored) {
257+
} catch (IllegalArgumentException ignored) {
258258
// nothing to do, try another format
259259
}
260260
}
@@ -263,8 +263,7 @@ private ExprValue parseTimeString(String value, OpenSearchDateType dateType) {
263263
try {
264264
return new ExprTimeValue(
265265
DateFormatters.from(STRICT_HOUR_MINUTE_SECOND_FORMATTER.parse(value)).toLocalTime());
266-
} catch (DateTimeParseException e) {
267-
// ignored
266+
} catch (DateTimeParseException ignored) {
268267
}
269268

270269
throw new IllegalArgumentException("Construct ExprTimeValue from \"" + value
@@ -289,7 +288,7 @@ private ExprValue parseDateString(String value, OpenSearchDateType dateType) {
289288
// return the first matching formatter as a date without timezone
290289
return new ExprDateValue(
291290
zonedDateTime.withZoneSameLocal(UTC_ZONE_ID).toLocalDate());
292-
} catch (IllegalArgumentException ignored) {
291+
} catch (IllegalArgumentException ignored) {
293292
// nothing to do, try another format
294293
}
295294
}
@@ -298,8 +297,7 @@ private ExprValue parseDateString(String value, OpenSearchDateType dateType) {
298297
try {
299298
return new ExprDateValue(
300299
DateFormatters.from(STRICT_YEAR_MONTH_DAY_FORMATTER.parse(value)).toLocalDate());
301-
} catch (DateTimeParseException e) {
302-
// ignored
300+
} catch (DateTimeParseException ignored) {
303301
}
304302

305303
throw new IllegalArgumentException("Construct ExprDateValue from \"" + value
@@ -310,7 +308,7 @@ private ExprValue createOpenSearchDateType(Content value, ExprType type) {
310308
OpenSearchDateType dt = (OpenSearchDateType) type;
311309
ExprType returnFormat = dt.getExprType();
312310

313-
if (value.isNumber()) {
311+
if (value.isNumber()) { // isNumber
314312
var numFormatters = dt.getNumericNamedFormatters();
315313
if (numFormatters.size() > 0) {
316314
long epochMillis = 0;
@@ -321,16 +319,14 @@ private ExprValue createOpenSearchDateType(Content value, ExprType type) {
321319
} else /* EPOCH_SECOND */ {
322320
epochMillis = value.longValue() * 1000;
323321
}
324-
Instant instant = Instant.ofEpochMilli(epochMillis);
325-
if (returnFormat == TIME) {
326-
return new ExprTimeValue(LocalTime.from(instant.atZone(UTC_ZONE_ID)));
327-
}
328-
if (returnFormat == DATE) {
329-
return new ExprDateValue(LocalDate.ofInstant(instant, UTC_ZONE_ID));
330-
}
331-
return new ExprTimestampValue(instant);
322+
return new ExprTimestampValue(Instant.ofEpochMilli(epochMillis));
332323
} else {
333-
return parseTimestampString(value.stringValue(), dt);
324+
// custom format
325+
switch ((ExprCoreType) dt.getExprType()) {
326+
case TIME: return parseTimeString(value.stringValue(), dt);
327+
case DATE: return parseDateString(value.stringValue(), dt);
328+
default: return parseTimestampString(value.stringValue(), dt);
329+
}
334330
}
335331
}
336332

0 commit comments

Comments
 (0)