Skip to content

Commit 853ca1a

Browse files
committed
add doc
Signed-off-by: xinyual <xinyual@amazon.com>
1 parent c986508 commit 853ca1a

5 files changed

Lines changed: 125 additions & 66 deletions

File tree

core/src/main/java/org/opensearch/sql/calcite/utils/PPLOperandTypes.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,6 @@ private PPLOperandTypes() {}
6161
public static final UDFOperandMetadata DATE_OR_TIMESTAMP_OR_STRING =
6262
UDFOperandMetadata.wrap(
6363
(CompositeOperandTypeChecker) OperandTypes.DATE_OR_TIMESTAMP.or(OperandTypes.STRING));
64-
public static final UDFOperandMetadata STRING_DATE_OR_TIMESTAMP =
65-
UDFOperandMetadata.wrap(
66-
(CompositeOperandTypeChecker)
67-
OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.DATE)
68-
.or(OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.TIMESTAMP)));
64+
public static final UDFOperandMetadata STRING_TIMESTAMP =
65+
UDFOperandMetadata.wrap(OperandTypes.family(SqlTypeFamily.STRING, SqlTypeFamily.TIMESTAMP));
6966
}

core/src/main/java/org/opensearch/sql/expression/function/udf/condition/EarliestFunction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
package org.opensearch.sql.expression.function.udf.condition;
77

8-
import static org.opensearch.sql.calcite.utils.PPLOperandTypes.STRING_DATE_OR_TIMESTAMP;
8+
import static org.opensearch.sql.calcite.utils.PPLOperandTypes.STRING_TIMESTAMP;
99
import static org.opensearch.sql.calcite.utils.UserDefinedFunctionUtils.prependFunctionProperties;
1010
import static org.opensearch.sql.utils.DateTimeUtils.getRelativeZonedDateTime;
1111

@@ -41,7 +41,7 @@ public SqlReturnTypeInference getReturnTypeInference() {
4141

4242
@Override
4343
public UDFOperandMetadata getOperandMetadata() {
44-
return STRING_DATE_OR_TIMESTAMP;
44+
return STRING_TIMESTAMP;
4545
}
4646

4747
public static class EarliestImplementor implements NotNullImplementor {

core/src/main/java/org/opensearch/sql/expression/function/udf/condition/LatestFunction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
package org.opensearch.sql.expression.function.udf.condition;
77

8-
import static org.opensearch.sql.calcite.utils.PPLOperandTypes.STRING_DATE_OR_TIMESTAMP;
8+
import static org.opensearch.sql.calcite.utils.PPLOperandTypes.STRING_TIMESTAMP;
99
import static org.opensearch.sql.calcite.utils.UserDefinedFunctionUtils.prependFunctionProperties;
1010
import static org.opensearch.sql.utils.DateTimeUtils.getRelativeZonedDateTime;
1111

@@ -41,7 +41,7 @@ public SqlReturnTypeInference getReturnTypeInference() {
4141

4242
@Override
4343
public UDFOperandMetadata getOperandMetadata() {
44-
return STRING_DATE_OR_TIMESTAMP;
44+
return STRING_TIMESTAMP;
4545
}
4646

4747
public static class LatestImplementor implements NotNullImplementor {

core/src/main/java/org/opensearch/sql/utils/DateTimeUtils.java

Lines changed: 91 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.time.format.DateTimeFormatter;
1515
import java.time.format.DateTimeParseException;
1616
import java.time.temporal.ChronoUnit;
17+
import java.util.Locale;
1718
import java.util.regex.Pattern;
1819
import lombok.experimental.UtilityClass;
1920
import org.opensearch.sql.data.model.ExprTimeValue;
@@ -162,9 +163,8 @@ public static LocalDate extractDate(ExprValue value, FunctionProperties function
162163

163164
public static ZonedDateTime getRelativeZonedDateTime(String input, ZonedDateTime baseTime) {
164165
try {
165-
Instant localDateTime =
166-
LocalDateTime.parse(input, DIRECT_FORMATTER).toInstant(ZoneOffset.UTC);
167-
return localDateTime.atZone(baseTime.getZone());
166+
Instant parsed = LocalDateTime.parse(input, DIRECT_FORMATTER).toInstant(ZoneOffset.UTC);
167+
return parsed.atZone(baseTime.getZone());
168168
} catch (DateTimeParseException ignored) {
169169
}
170170

@@ -177,85 +177,121 @@ public static ZonedDateTime getRelativeZonedDateTime(String input, ZonedDateTime
177177
while (i < input.length()) {
178178
char c = input.charAt(i);
179179
if (c == '@') {
180-
// parse snap
181180
int j = i + 1;
182-
while (j < input.length() && Character.isLetter(input.charAt(j))) {
181+
while (j < input.length() && Character.isLetterOrDigit(input.charAt(j))) {
183182
j++;
184183
}
185-
String snapUnit = input.substring(i + 1, j);
186-
result = applySnap(result, snapUnit);
184+
String rawUnit = input.substring(i + 1, j);
185+
result = applySnap(result, rawUnit);
187186
i = j;
188187
} else if (c == '+' || c == '-') {
189-
// parse offset
190188
int j = i + 1;
191189
while (j < input.length() && Character.isDigit(input.charAt(j))) {
192190
j++;
193191
}
194-
int value = Integer.parseInt(input.substring(i + 1, j));
195-
// optional unit
192+
String valueStr = input.substring(i + 1, j);
193+
int value = valueStr.isEmpty() ? 1 : Integer.parseInt(valueStr);
194+
196195
int k = j;
197196
while (k < input.length() && Character.isLetter(input.charAt(k))) {
198197
k++;
199198
}
200-
String unit = input.substring(j, k);
201-
if (unit.isEmpty()) {
202-
unit = "s"; // default to seconds
203-
}
204-
result = applyOffset(result, String.valueOf(c), value, unit);
199+
String rawUnit = input.substring(j, k);
200+
result = applyOffset(result, String.valueOf(c), value, rawUnit);
205201
i = k;
206202
} else {
207-
throw new IllegalArgumentException("Wrong relative time expression: " + input);
203+
throw new IllegalArgumentException(
204+
"Unexpected character '" + c + "' at position " + i + " in input: " + input);
208205
}
209206
}
210207

211208
return result;
212209
}
213210

214211
private static ZonedDateTime applyOffset(
215-
ZonedDateTime base, String sign, int value, String unit) {
216-
ChronoUnit chronoUnit = parseUnit(unit);
212+
ZonedDateTime base, String sign, int value, String rawUnit) {
213+
String unit = normalizeUnit(rawUnit);
214+
if ("q".equals(unit)) {
215+
int months = value * 3;
216+
return sign.equals("-") ? base.minusMonths(months) : base.plusMonths(months);
217+
}
218+
219+
ChronoUnit chronoUnit =
220+
switch (unit) {
221+
case "s" -> ChronoUnit.SECONDS;
222+
case "m" -> ChronoUnit.MINUTES;
223+
case "h" -> ChronoUnit.HOURS;
224+
case "d" -> ChronoUnit.DAYS;
225+
case "w" -> ChronoUnit.WEEKS;
226+
case "M" -> ChronoUnit.MONTHS;
227+
case "y" -> ChronoUnit.YEARS;
228+
default -> throw new IllegalArgumentException("Unsupported offset unit: " + rawUnit);
229+
};
230+
217231
return sign.equals("-") ? base.minus(value, chronoUnit) : base.plus(value, chronoUnit);
218232
}
219233

220-
private static ZonedDateTime applySnap(ZonedDateTime base, String unit) {
221-
switch (unit) {
222-
case "s":
223-
return base.truncatedTo(ChronoUnit.SECONDS);
224-
case "m":
225-
return base.truncatedTo(ChronoUnit.MINUTES);
226-
case "h":
227-
return base.truncatedTo(ChronoUnit.HOURS);
228-
case "d":
229-
return base.truncatedTo(ChronoUnit.DAYS);
230-
case "w":
231-
return base.minusDays((base.getDayOfWeek().getValue() % 7)).truncatedTo(ChronoUnit.DAYS);
232-
case "M":
233-
return base.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
234-
case "y":
235-
return base.withDayOfYear(1).truncatedTo(ChronoUnit.DAYS);
236-
default:
237-
throw new IllegalArgumentException("Unsupported snap unit: " + unit);
238-
}
234+
private static ZonedDateTime applySnap(ZonedDateTime base, String rawUnit) {
235+
String unit = normalizeUnit(rawUnit);
236+
237+
return switch (unit) {
238+
case "s" -> base.truncatedTo(ChronoUnit.SECONDS);
239+
case "m" -> base.truncatedTo(ChronoUnit.MINUTES);
240+
case "h" -> base.truncatedTo(ChronoUnit.HOURS);
241+
case "d" -> base.truncatedTo(ChronoUnit.DAYS);
242+
case "w" -> base.minusDays((base.getDayOfWeek().getValue() % 7)).truncatedTo(ChronoUnit.DAYS);
243+
case "M" -> base.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
244+
case "y" -> base.withDayOfYear(1).truncatedTo(ChronoUnit.DAYS);
245+
case "q" -> {
246+
int month = base.getMonthValue();
247+
int quarterStart = ((month - 1) / 3) * 3 + 1;
248+
yield base.withMonth(quarterStart).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
249+
}
250+
default -> {
251+
if (unit.matches("w[0-7]")) {
252+
int targetDay =
253+
unit.equals("w0") || unit.equals("w7") ? 7 : Integer.parseInt(unit.substring(1));
254+
int diff = (base.getDayOfWeek().getValue() - targetDay + 7) % 7;
255+
yield base.minusDays(diff).truncatedTo(ChronoUnit.DAYS);
256+
} else {
257+
throw new IllegalArgumentException("Unsupported snap unit: " + rawUnit);
258+
}
259+
}
260+
};
239261
}
240262

241-
private static ChronoUnit parseUnit(String unit) {
242-
switch (unit) {
243-
case "s":
244-
return ChronoUnit.SECONDS;
245-
case "m":
246-
return ChronoUnit.MINUTES;
247-
case "h":
248-
return ChronoUnit.HOURS;
249-
case "d":
250-
return ChronoUnit.DAYS;
251-
case "w":
252-
return ChronoUnit.WEEKS;
253-
case "M":
254-
return ChronoUnit.MONTHS;
255-
case "y":
256-
return ChronoUnit.YEARS;
257-
default:
258-
throw new IllegalArgumentException("Unsupported time unit: " + unit);
263+
private static String normalizeUnit(String rawUnit) {
264+
// strict minute (m or M)
265+
switch (rawUnit.toLowerCase(Locale.ROOT)) {
266+
case "m", "min", "mins", "minute", "minutes" -> {
267+
return "m";
268+
}
269+
case "s", "sec", "secs", "second", "seconds" -> {
270+
return "s";
271+
}
272+
case "h", "hr", "hrs", "hour", "hours" -> {
273+
return "h";
274+
}
275+
case "d", "day", "days" -> {
276+
return "d";
277+
}
278+
case "w", "wk", "wks", "week", "weeks" -> {
279+
return "w";
280+
}
281+
case "mon", "month", "months" -> {
282+
return "M"; // month
283+
}
284+
case "y", "yr", "yrs", "year", "years" -> {
285+
return "y";
286+
}
287+
case "q", "qtr", "qtrs", "quarter", "quarters" -> {
288+
return "q";
289+
}
290+
default -> {
291+
String lower = rawUnit.toLowerCase();
292+
if (lower.matches("w[0-7]")) return lower;
293+
throw new IllegalArgumentException("Unsupported unit alias: " + rawUnit);
294+
}
259295
}
260296
}
261297
}

core/src/test/java/org/opensearch/sql/utils/DateTimeUtilsTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,38 @@ void testRelativeZonedDateTimeWithOffset() {
6161

6262
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeString, formatter);
6363
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
64-
ZonedDateTime snap1 = getRelativeZonedDateTime("-1d+1y@M", zonedDateTime);
64+
ZonedDateTime snap1 = getRelativeZonedDateTime("-1d+1y@mon", zonedDateTime);
6565
ZonedDateTime snap2 = getRelativeZonedDateTime("-3d@d-2h+10m@h", zonedDateTime);
6666
assertEquals(snap1.toLocalDateTime().toString(), "2026-10-01T00:00");
6767
assertEquals(snap2.toLocalDateTime().toString(), "2025-10-18T22:00");
6868
}
6969

70+
@Test
71+
void testRelativeZonedDateTimeWithAlias() {
72+
String dateTimeString = "2025-10-22 10:32:12";
73+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
74+
75+
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeString, formatter);
76+
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
77+
ZonedDateTime snap1 = getRelativeZonedDateTime("-1d+1y@Month", zonedDateTime);
78+
ZonedDateTime snap2 = getRelativeZonedDateTime("-3d@d-2h+10m@hours", zonedDateTime);
79+
assertEquals(snap1.toLocalDateTime().toString(), "2026-10-01T00:00");
80+
assertEquals(snap2.toLocalDateTime().toString(), "2025-10-18T22:00");
81+
}
82+
83+
@Test
84+
void testRelativeZonedDateTimeWithWeekDayAndQuarter() {
85+
String dateTimeString = "2025-10-22 10:32:12";
86+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
87+
88+
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeString, formatter);
89+
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
90+
ZonedDateTime snap1 = getRelativeZonedDateTime("-1d+1y@W5", zonedDateTime);
91+
ZonedDateTime snap2 = getRelativeZonedDateTime("-3d@d-2q+10m@quarter", zonedDateTime);
92+
assertEquals(snap1.toLocalDateTime().toString(), "2026-10-16T00:00");
93+
assertEquals(snap2.toLocalDateTime().toString(), "2025-04-01T00:00");
94+
}
95+
7096
@Test
7197
void testRelativeZonedDateTimeWithWrongInput() {
7298
String dateTimeString = "2025-10-22 10:32:12";
@@ -77,6 +103,6 @@ void testRelativeZonedDateTimeWithWrongInput() {
77103
IllegalArgumentException e =
78104
assertThrows(
79105
IllegalArgumentException.class, () -> getRelativeZonedDateTime("1d+1y", zonedDateTime));
80-
assertEquals(e.getMessage(), "Wrong relative time expression: 1d+1y");
106+
assertEquals(e.getMessage(), "Unexpected character '1' at position 0 in input: 1d+1y");
81107
}
82108
}

0 commit comments

Comments
 (0)