Skip to content

Commit 1d458e3

Browse files
authored
SQL: Make INTERVAL millis optional (#36043)
Fractions of the second are not mandatory anymore inside INTERVAL declarations Fix #36032
1 parent c305f9d commit 1d458e3

File tree

2 files changed

+64
-14
lines changed
  • x-pack/plugin/sql/src

2 files changed

+64
-14
lines changed

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/literal/Intervals.java

+28-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package org.elasticsearch.xpack.sql.expression.literal;
88

9+
import org.elasticsearch.common.Strings;
910
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1011
import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry;
1112
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
@@ -124,6 +125,7 @@ private static class ParserBuilder {
124125
private final List<TimeUnit> units;
125126
private final List<Token> tokens;
126127
private final String name;
128+
private boolean optional = false;
127129

128130
ParserBuilder(DataType dataType) {
129131
units = new ArrayList<>(10);
@@ -138,12 +140,17 @@ ParserBuilder unit(TimeUnit unit) {
138140

139141
ParserBuilder unit(TimeUnit unit, int maxValue) {
140142
units.add(unit);
141-
tokens.add(new Token((char) 0, maxValue));
143+
tokens.add(new Token((char) 0, maxValue, optional));
142144
return this;
143145
}
144146

145147
ParserBuilder separator(char ch) {
146-
tokens.add(new Token(ch, 0));
148+
tokens.add(new Token(ch, 0, optional));
149+
return this;
150+
}
151+
152+
ParserBuilder optional() {
153+
optional = true;
147154
return this;
148155
}
149156

@@ -155,15 +162,17 @@ Parser build() {
155162
private static class Token {
156163
private final char ch;
157164
private final int maxValue;
165+
private final boolean optional;
158166

159-
Token(char ch, int maxValue) {
167+
Token(char ch, int maxValue, boolean optional) {
160168
this.ch = ch;
161169
this.maxValue = maxValue;
170+
this.optional = optional;
162171
}
163172

164173
@Override
165174
public String toString() {
166-
return ch > 0 ? String.valueOf(ch) : "[numeric" + (maxValue > 0 ? " < " + maxValue + " " : "") + "]";
175+
return ch > 0 ? String.valueOf(ch) : "[numeric]";
167176
}
168177
}
169178

@@ -203,6 +212,15 @@ TemporalAmount parse(Location source, String string) {
203212
for (Token token : tokens) {
204213
endToken = startToken;
205214

215+
if (startToken >= string.length()) {
216+
// consumed the string, bail out
217+
if (token.optional) {
218+
break;
219+
}
220+
throw new ParsingException(source, invalidIntervalMessage(string) + ": incorrect format, expecting {}",
221+
Strings.collectionToDelimitedString(tokens, ""));
222+
}
223+
206224
// char token
207225
if (token.ch != 0) {
208226
char found = string.charAt(startToken);
@@ -309,8 +327,8 @@ public static TemporalAmount negate(TemporalAmount interval) {
309327
PARSERS.put(DataType.INTERVAL_MINUTE, new ParserBuilder(DataType.INTERVAL_MINUTE).unit(TimeUnit.MINUTE).build());
310328
PARSERS.put(DataType.INTERVAL_SECOND, new ParserBuilder(DataType.INTERVAL_SECOND)
311329
.unit(TimeUnit.SECOND)
312-
.separator(DOT)
313-
.unit(TimeUnit.MILLISECOND, MAX_MILLI)
330+
.optional()
331+
.separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI)
314332
.build());
315333

316334
// patterns
@@ -342,6 +360,7 @@ public static TemporalAmount negate(TemporalAmount interval) {
342360
.unit(TimeUnit.MINUTE, MAX_MINUTE)
343361
.separator(COLON)
344362
.unit(TimeUnit.SECOND, MAX_SECOND)
363+
.optional()
345364
.separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI)
346365
.build());
347366

@@ -357,15 +376,16 @@ public static TemporalAmount negate(TemporalAmount interval) {
357376
.unit(TimeUnit.MINUTE, MAX_MINUTE)
358377
.separator(COLON)
359378
.unit(TimeUnit.SECOND, MAX_SECOND)
379+
.optional()
360380
.separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI)
361381
.build());
362382

363383
PARSERS.put(DataType.INTERVAL_MINUTE_TO_SECOND, new ParserBuilder(DataType.INTERVAL_MINUTE_TO_SECOND)
364384
.unit(TimeUnit.MINUTE)
365385
.separator(COLON)
366386
.unit(TimeUnit.SECOND, MAX_SECOND)
367-
.separator(DOT)
368-
.unit(TimeUnit.MILLISECOND, MAX_MILLI)
387+
.optional()
388+
.separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI)
369389
.build());
370390
}
371391

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/literal/IntervalsTests.java

+36-6
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ public void testSecondInterval() throws Exception {
8989
assertEquals(maybeNegate(sign, Duration.ofSeconds(randomSeconds).plusMillis(randomMillis)), amount);
9090
}
9191

92+
public void testSecondNoMillisInterval() throws Exception {
93+
int randomSeconds = randomNonNegativeInt();
94+
String value = format(Locale.ROOT, "%s%d", sign, randomSeconds);
95+
TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_SECOND);
96+
assertEquals(maybeNegate(sign, Duration.ofSeconds(randomSeconds)), amount);
97+
}
98+
9299
public void testYearToMonth() throws Exception {
93100
int randomYear = randomNonNegativeInt();
94101
int randomMonth = randomInt(11);
@@ -119,9 +126,12 @@ public void testDayToSecond() throws Exception {
119126
int randomHour = randomInt(23);
120127
int randomMinute = randomInt(59);
121128
int randomSecond = randomInt(59);
122-
int randomMilli = randomInt(999999999);
123129

124-
String value = format(Locale.ROOT, "%s%d %d:%d:%d.%d", sign, randomDay, randomHour, randomMinute, randomSecond, randomMilli);
130+
boolean withMillis = randomBoolean();
131+
int randomMilli = withMillis ? randomInt(999999999) : 0;
132+
String millisString = withMillis ? "." + randomMilli : "";
133+
134+
String value = format(Locale.ROOT, "%s%d %d:%d:%d%s", sign, randomDay, randomHour, randomMinute, randomSecond, millisString);
125135
TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_DAY_TO_SECOND);
126136
assertEquals(maybeNegate(sign, Duration.ofDays(randomDay).plusHours(randomHour).plusMinutes(randomMinute)
127137
.plusSeconds(randomSecond).plusMillis(randomMilli)), amount);
@@ -139,9 +149,12 @@ public void testHourToSecond() throws Exception {
139149
int randomHour = randomNonNegativeInt();
140150
int randomMinute = randomInt(59);
141151
int randomSecond = randomInt(59);
142-
int randomMilli = randomInt(999999999);
143152

144-
String value = format(Locale.ROOT, "%s%d:%d:%d.%d", sign, randomHour, randomMinute, randomSecond, randomMilli);
153+
boolean withMillis = randomBoolean();
154+
int randomMilli = withMillis ? randomInt(999999999) : 0;
155+
String millisString = withMillis ? "." + randomMilli : "";
156+
157+
String value = format(Locale.ROOT, "%s%d:%d:%d%s", sign, randomHour, randomMinute, randomSecond, millisString);
145158
TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_HOUR_TO_SECOND);
146159
assertEquals(maybeNegate(sign,
147160
Duration.ofHours(randomHour).plusMinutes(randomMinute).plusSeconds(randomSecond).plusMillis(randomMilli)), amount);
@@ -150,9 +163,12 @@ public void testHourToSecond() throws Exception {
150163
public void testMinuteToSecond() throws Exception {
151164
int randomMinute = randomNonNegativeInt();
152165
int randomSecond = randomInt(59);
153-
int randomMilli = randomInt(999999999);
154166

155-
String value = format(Locale.ROOT, "%s%d:%d.%d", sign, randomMinute, randomSecond, randomMilli);
167+
boolean withMillis = randomBoolean();
168+
int randomMilli = withMillis ? randomInt(999999999) : 0;
169+
String millisString = withMillis ? "." + randomMilli : "";
170+
171+
String value = format(Locale.ROOT, "%s%d:%d%s", sign, randomMinute, randomSecond, millisString);
156172
TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_MINUTE_TO_SECOND);
157173
assertEquals(maybeNegate(sign, Duration.ofMinutes(randomMinute).plusSeconds(randomSecond).plusMillis(randomMilli)), amount);
158174
}
@@ -187,6 +203,20 @@ public void testDayToMinuteTooBig() throws Exception {
187203
+ "], expected a positive number up to [23]", pe.getMessage());
188204
}
189205

206+
public void testIncompleteYearToMonthInterval() throws Exception {
207+
String value = "123-";
208+
ParsingException pe = expectThrows(ParsingException.class, () -> parseInterval(EMPTY, value, INTERVAL_YEAR_TO_MONTH));
209+
assertEquals("line -1:0: Invalid [INTERVAL YEAR TO MONTH] value [123-]: incorrect format, expecting [numeric]-[numeric]",
210+
pe.getMessage());
211+
}
212+
213+
public void testIncompleteDayToHourInterval() throws Exception {
214+
String value = "123 23:";
215+
ParsingException pe = expectThrows(ParsingException.class, () -> parseInterval(EMPTY, value, INTERVAL_DAY_TO_HOUR));
216+
assertEquals("line -1:0: Invalid [INTERVAL DAY TO HOUR] value [123 23:]: unexpected trailing characters found [:]",
217+
pe.getMessage());
218+
}
219+
190220
public void testExtraCharLeading() throws Exception {
191221
String value = "a123";
192222
ParsingException pe = expectThrows(ParsingException.class, () -> parseInterval(EMPTY, value, INTERVAL_YEAR));

0 commit comments

Comments
 (0)