1515import static org .opensearch .sql .util .MatcherUtils .verifySchema ;
1616
1717import java .io .IOException ;
18+ import java .time .LocalDateTime ;
19+ import java .time .ZoneOffset ;
20+ import java .time .format .DateTimeFormatter ;
21+ import java .time .temporal .ChronoUnit ;
1822import org .json .JSONObject ;
1923import org .junit .jupiter .api .Test ;
2024import org .opensearch .client .Request ;
2125import org .opensearch .client .ResponseException ;
26+ import org .opensearch .sql .common .utils .StringUtils ;
2227
2328public class SearchCommandIT extends PPLIntegTestCase {
29+ private static final DateTimeFormatter PPL_TIMESTAMP_FORMATTER =
30+ DateTimeFormatter .ofPattern ("yyyy-MM-dd HH:mm:ss" );
2431
2532 @ Override
2633 public void init () throws Exception {
@@ -1025,7 +1032,7 @@ public void testSearchWithChainedRelativeTimeRange() throws IOException {
10251032 JSONObject result1 =
10261033 executeQuery (
10271034 String .format (
1028- "search source=%s earliest='2025-08-01 03:47:41' latest=+10months @year | fields"
1035+ "search source=%s earliest='2025-08-01 03:47:41' latest=+1months @year | fields"
10291036 + " @timestamp" ,
10301037 TEST_INDEX_TIME_DATA ));
10311038 verifySchema (result1 , schema ("@timestamp" , "timestamp" ));
@@ -1042,4 +1049,275 @@ public void testSearchWithNumericTimeRange() throws IOException {
10421049 verifySchema (result1 , schema ("@timestamp" , "timestamp" ));
10431050 verifyDataRows (result1 , rows ("2025-08-01 03:47:41" ));
10441051 }
1052+
1053+ @ Test
1054+ public void testSearchTimeModifierWithSnappedWeek () throws IOException {
1055+ // Test whether alignment to weekday works
1056+
1057+ final int docId = 101 ;
1058+ Request insertRequest =
1059+ new Request ("PUT" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1060+
1061+ // Get the current weekday
1062+ LocalDateTime base = LocalDateTime .now (ZoneOffset .UTC );
1063+ // Truncate to last sunday
1064+ LocalDateTime lastSunday =
1065+ base .minusDays ((base .getDayOfWeek ().getValue () % 7 )).truncatedTo (ChronoUnit .DAYS );
1066+ LocalDateTime lastMonday =
1067+ base .minusDays ((base .getDayOfWeek ().getValue () + 6 ) % 7 ).truncatedTo (ChronoUnit .DAYS );
1068+ LocalDateTime lastFriday =
1069+ base .minusDays ((base .getDayOfWeek ().getValue () + 2 ) % 7 ).truncatedTo (ChronoUnit .DAYS );
1070+
1071+ // Insert data
1072+ LocalDateTime insertedSunday = lastSunday .plusMinutes (10 );
1073+ insertRequest .setJsonEntity (
1074+ StringUtils .format (
1075+ "{\" value\" :100090,\" category\" :\" A\" ,\" @timestamp\" :\" %s\" }\n " ,
1076+ insertedSunday .format (DateTimeFormatter .ISO_DATE_TIME )));
1077+ client ().performRequest (insertRequest );
1078+
1079+ JSONObject result1 =
1080+ executeQuery (String .format ("source=%s earliest=@w0 latest='@w+1h'" , TEST_INDEX_TIME_DATA ));
1081+ verifySchema (
1082+ result1 ,
1083+ schema ("timestamp" , "timestamp" ),
1084+ schema ("value" , "int" ),
1085+ schema ("category" , "string" ),
1086+ schema ("@timestamp" , "timestamp" ));
1087+ verifyDataRows (
1088+ result1 , rows (insertedSunday .format (PPL_TIMESTAMP_FORMATTER ), "A" , 100090 , null ));
1089+
1090+ // Test last Monday
1091+ LocalDateTime insertedMonday = lastMonday .plusHours (2 ).plusMinutes (10 );
1092+ Request insertRequest2 =
1093+ new Request ("PUT" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1094+ insertRequest2 .setJsonEntity (
1095+ StringUtils .format (
1096+ "{\" value\" :100091,\" category\" :\" B\" ,\" @timestamp\" :\" %s\" }\n " ,
1097+ insertedMonday .format (DateTimeFormatter .ISO_DATE_TIME )));
1098+ client ().performRequest (insertRequest2 );
1099+
1100+ JSONObject result2 =
1101+ executeQuery (
1102+ String .format (
1103+ "source=%s earliest='@w1+2h' latest='@w1+2h+30min'" , TEST_INDEX_TIME_DATA ));
1104+ verifySchema (
1105+ result2 ,
1106+ schema ("timestamp" , "timestamp" ),
1107+ schema ("value" , "int" ),
1108+ schema ("category" , "string" ),
1109+ schema ("@timestamp" , "timestamp" ));
1110+ verifyDataRows (
1111+ result2 , rows (insertedMonday .format (PPL_TIMESTAMP_FORMATTER ), "B" , 100091 , null ));
1112+
1113+ // Test last Friday
1114+ LocalDateTime insertedFriday = lastFriday .plusMinutes (10 );
1115+ Request insertRequest3 =
1116+ new Request ("PUT" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1117+ insertRequest3 .setJsonEntity (
1118+ StringUtils .format (
1119+ "{\" value\" :100092,\" category\" :\" C\" ,\" @timestamp\" :\" %s\" }\n " ,
1120+ insertedFriday .format (DateTimeFormatter .ISO_DATE_TIME )));
1121+ client ().performRequest (insertRequest3 );
1122+
1123+ JSONObject result3 =
1124+ executeQuery (
1125+ String .format ("source=%s earliest=@w5 latest='@w5+30minutes'" , TEST_INDEX_TIME_DATA ));
1126+ verifySchema (
1127+ result3 ,
1128+ schema ("timestamp" , "timestamp" ),
1129+ schema ("value" , "int" ),
1130+ schema ("category" , "string" ),
1131+ schema ("@timestamp" , "timestamp" ));
1132+ verifyDataRows (
1133+ result3 , rows (insertedFriday .format (PPL_TIMESTAMP_FORMATTER ), "C" , 100092 , null ));
1134+
1135+ Request deleteRequest =
1136+ new Request (
1137+ "DELETE" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1138+ client ().performRequest (deleteRequest );
1139+ }
1140+
1141+ @ Test
1142+ public void testSearchWithRelativeTimeModifiers () throws IOException {
1143+ final int docId = 101 ;
1144+
1145+ LocalDateTime currentTime = LocalDateTime .now (ZoneOffset .UTC );
1146+ LocalDateTime testTime = currentTime .minusMinutes (30 ).truncatedTo (ChronoUnit .MINUTES );
1147+
1148+ Request insertRequest =
1149+ new Request ("PUT" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1150+ insertRequest .setJsonEntity (
1151+ StringUtils .format (
1152+ "{\" value\" :200001,\" category\" :\" RELATIVE\" ,\" @timestamp\" :\" %s\" }\n " ,
1153+ testTime .format (DateTimeFormatter .ISO_DATE_TIME )));
1154+ client ().performRequest (insertRequest );
1155+
1156+ // Test -1h (1 hour ago) since it's only 30 minutes old
1157+ JSONObject result1 =
1158+ executeQuery (
1159+ String .format (
1160+ "source=%s earliest=-1h | fields @timestamp, value | head 5" ,
1161+ TEST_INDEX_TIME_DATA ));
1162+ verifySchema (result1 , schema ("@timestamp" , "timestamp" ), schema ("value" , "int" ));
1163+ verifyDataRows (result1 , rows (testTime .format (PPL_TIMESTAMP_FORMATTER ), 200001 ));
1164+
1165+ // Test -30m (30 minutes ago)
1166+ JSONObject result2 =
1167+ executeQuery (
1168+ String .format (
1169+ "source=%s earliest=-50m latest=now | fields @timestamp, value | head 5" ,
1170+ TEST_INDEX_TIME_DATA ));
1171+ verifySchema (result2 , schema ("@timestamp" , "timestamp" ), schema ("value" , "int" ));
1172+ verifyDataRows (result2 , rows (testTime .format (PPL_TIMESTAMP_FORMATTER ), 200001 ));
1173+
1174+ // Test -7d (7 days ago) - should return no results since our data is recent
1175+ JSONObject result3 =
1176+ executeQuery (
1177+ String .format (
1178+ "source=%s earliest=-7d latest=-6d | fields @timestamp | head 1" ,
1179+ TEST_INDEX_TIME_DATA ));
1180+ verifySchema (result3 , schema ("@timestamp" , "timestamp" ));
1181+ verifyNumOfRows (result3 , 0 );
1182+
1183+ Request deleteRequest =
1184+ new Request (
1185+ "DELETE" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1186+ client ().performRequest (deleteRequest );
1187+ }
1188+
1189+ @ Test
1190+ public void testSearchWithTimeUnitSnapping () throws IOException {
1191+ final int docId = 101 ;
1192+
1193+ LocalDateTime currentHour = LocalDateTime .now (ZoneOffset .UTC ).truncatedTo (ChronoUnit .HOURS );
1194+ LocalDateTime testTime = currentHour .plusMinutes (15 ).truncatedTo (ChronoUnit .MINUTES );
1195+
1196+ Request insertRequest =
1197+ new Request ("PUT" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1198+ insertRequest .setJsonEntity (
1199+ StringUtils .format (
1200+ "{\" value\" :200002,\" category\" :\" SNAP\" ,\" @timestamp\" :\" %s\" }\n " ,
1201+ testTime .format (DateTimeFormatter .ISO_DATE_TIME )));
1202+ client ().performRequest (insertRequest );
1203+
1204+ // Test @h (snap to hour)
1205+ JSONObject result1 =
1206+ executeQuery (
1207+ String .format (
1208+ "source=%s earliest=@h latest='@h+1h' | fields @timestamp, value" ,
1209+ TEST_INDEX_TIME_DATA ));
1210+ verifySchema (result1 , schema ("@timestamp" , "timestamp" ), schema ("value" , "int" ));
1211+ verifyDataRows (result1 , rows (testTime .format (PPL_TIMESTAMP_FORMATTER ), 200002 ));
1212+
1213+ // Test @d (snap to day)
1214+ JSONObject result2 =
1215+ executeQuery (
1216+ String .format (
1217+ "source=%s earliest=@d latest='@d+1d' | fields @timestamp | head 5" ,
1218+ TEST_INDEX_TIME_DATA ));
1219+ verifySchema (result2 , schema ("@timestamp" , "timestamp" ));
1220+ verifyDataRows (result2 , rows (testTime .format (PPL_TIMESTAMP_FORMATTER )));
1221+
1222+ // Test @M (snap to month)
1223+ JSONObject result3 =
1224+ executeQuery (
1225+ String .format (
1226+ "source=%s earliest=@mon latest='@mon+1mon' | fields @timestamp | head 5" ,
1227+ TEST_INDEX_TIME_DATA ));
1228+ verifySchema (result3 , schema ("@timestamp" , "timestamp" ));
1229+ verifyDataRows (result3 , rows (testTime .format (PPL_TIMESTAMP_FORMATTER )));
1230+
1231+ Request deleteRequest =
1232+ new Request (
1233+ "DELETE" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1234+ client ().performRequest (deleteRequest );
1235+ }
1236+
1237+ @ Test
1238+ public void testSearchWithQuarterlyModifiers () throws IOException {
1239+ final int docId = 101 ;
1240+
1241+ LocalDateTime currentQuarter =
1242+ LocalDateTime .now (ZoneOffset .UTC )
1243+ .plusYears (1 )
1244+ .withMonth (((LocalDateTime .now (ZoneOffset .UTC ).getMonthValue () - 1 ) / 3 ) * 3 + 1 )
1245+ .withDayOfMonth (1 )
1246+ .truncatedTo (ChronoUnit .DAYS );
1247+ LocalDateTime testTime = currentQuarter .plusDays (15 );
1248+
1249+ Request insertRequest =
1250+ new Request ("PUT" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1251+ insertRequest .setJsonEntity (
1252+ StringUtils .format (
1253+ "{\" value\" :200003,\" category\" :\" QUARTER\" ,\" @timestamp\" :\" %s\" }\n " ,
1254+ testTime .format (DateTimeFormatter .ISO_DATE_TIME )));
1255+ client ().performRequest (insertRequest );
1256+
1257+ // Test @q (snap to quarter)
1258+ JSONObject result1 =
1259+ executeQuery (
1260+ String .format (
1261+ "source=%s earliest=+1year@q latest='+1year@q+3M' | fields @timestamp, value" ,
1262+ TEST_INDEX_TIME_DATA ));
1263+ verifySchema (result1 , schema ("@timestamp" , "timestamp" ), schema ("value" , "int" ));
1264+ verifyDataRows (result1 , rows (testTime .format (PPL_TIMESTAMP_FORMATTER ), 200003 ));
1265+
1266+ // Test -2q (2 quarters ago, equivalent to -6M), should return no data
1267+ JSONObject result2 =
1268+ executeQuery (
1269+ String .format (
1270+ "source=%s earliest='+1year-2q' latest='+1year-1q' | fields @timestamp | head 1" ,
1271+ TEST_INDEX_TIME_DATA ));
1272+ verifySchema (result2 , schema ("@timestamp" , "timestamp" ));
1273+ verifyNumOfRows (result2 , 0 );
1274+
1275+ Request deleteRequest =
1276+ new Request (
1277+ "DELETE" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1278+ client ().performRequest (deleteRequest );
1279+ }
1280+
1281+ @ Test
1282+ public void testSearchWithComplexChainedExpressions () throws IOException {
1283+ final int docId = 101 ;
1284+
1285+ LocalDateTime lastDay =
1286+ LocalDateTime .now (ZoneOffset .UTC ).minusDays (1 ).truncatedTo (ChronoUnit .DAYS );
1287+ LocalDateTime testTime = lastDay .minusHours (2 ).plusMinutes (10 );
1288+
1289+ Request insertRequest =
1290+ new Request ("PUT" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1291+ insertRequest .setJsonEntity (
1292+ StringUtils .format (
1293+ "{\" value\" :200004,\" category\" :\" COMPLEX\" ,\" @timestamp\" :\" %s\" }\n " ,
1294+ testTime .format (DateTimeFormatter .ISO_DATE_TIME )));
1295+ client ().performRequest (insertRequest );
1296+
1297+ // Test -1d@d-2h+10m (1 day ago at day start, minus 2 hours, plus 10 minutes)
1298+ JSONObject result1 =
1299+ executeQuery (
1300+ String .format (
1301+ "source=%s earliest='-1d@d-2h' latest='-1d@d-2h+20m' | fields @timestamp,"
1302+ + " value" ,
1303+ TEST_INDEX_TIME_DATA ));
1304+ verifySchema (result1 , schema ("@timestamp" , "timestamp" ), schema ("value" , "int" ));
1305+ verifyDataRows (result1 , rows (testTime .format (PPL_TIMESTAMP_FORMATTER ), 200004 ));
1306+
1307+ // Test -1mon@mon+7d (1 month ago at month start, plus 7 days) - should return no results since
1308+ // our data is recent
1309+ JSONObject result2 =
1310+ executeQuery (
1311+ String .format (
1312+ "source=%s earliest='-1mon@mon+7d' latest='-1mon@mon+8d' | fields @timestamp | head"
1313+ + " 1" ,
1314+ TEST_INDEX_TIME_DATA ));
1315+ verifySchema (result2 , schema ("@timestamp" , "timestamp" ));
1316+ verifyNumOfRows (result2 , 0 );
1317+
1318+ Request deleteRequest =
1319+ new Request (
1320+ "DELETE" , String .format ("/%s/_doc/%d?refresh=true" , TEST_INDEX_TIME_DATA , docId ));
1321+ client ().performRequest (deleteRequest );
1322+ }
10451323}
0 commit comments