Skip to content

Commit 9602c9d

Browse files
committed
Support unixtimestamp (second) as a time modifier value
Signed-off-by: Yuanchun Shen <[email protected]>
1 parent b3a7ee2 commit 9602c9d

File tree

10 files changed

+108
-20
lines changed

10 files changed

+108
-20
lines changed

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,6 @@ void testParseRelativeTimeNull() {
175175
assertNull(parseRelativeTime(""));
176176
}
177177

178-
@Test
179-
void testParseRelativeTimeWithNow() {
180-
assertEquals("now", parseRelativeTime("now"));
181-
assertEquals("now", parseRelativeTime("now()"));
182-
}
183-
184178
@Test
185179
void testParseRelativeTimeWithDatetimeString() {
186180
assertEquals("2025-10-22", parseRelativeTime("2025-10-22"));

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java

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

66
package org.opensearch.sql.calcite.remote;
77

8-
import static org.junit.Assert.assertTrue;
9-
import static org.opensearch.sql.legacy.TestUtils.*;
108
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK;
119
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_LOGS;
1210
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_NESTED_SIMPLE;

integ-test/src/test/java/org/opensearch/sql/ppl/ExplainIT.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,15 @@ public void testSearchCommandWithRelativeTimeRange() throws IOException {
649649
String.format("source=%s earliest='-1q@year' latest=now", TEST_INDEX_TIME_DATA)));
650650
}
651651

652+
@Test
653+
public void testSearchCommandWithNumericTimeRange() throws IOException {
654+
String expected = loadExpectedPlan("explain_search_with_numeric_time_range.json");
655+
assertJsonEqualsIgnoreId(
656+
expected,
657+
explainQueryToString(
658+
String.format("source=%s earliest=1 latest=1754020061.123456", TEST_INDEX_TIME_DATA)));
659+
}
660+
652661
protected String loadExpectedPlan(String fileName) throws IOException {
653662
String prefix;
654663
if (isCalciteEnabled()) {

integ-test/src/test/java/org/opensearch/sql/ppl/SearchCommandIT.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,4 +958,15 @@ public void testSearchWithChainedRelativeTimeRange() throws IOException {
958958
verifySchema(result1, schema("@timestamp", "timestamp"));
959959
verifyDataRows(result1, rows("2025-08-01 03:47:41"));
960960
}
961+
962+
@Test
963+
public void testSearchWithNumericTimeRange() throws IOException {
964+
JSONObject result1 =
965+
executeQuery(
966+
String.format(
967+
"search source=%s earliest=1754020060.123 latest=1754020061 | fields @timestamp",
968+
TEST_INDEX_TIME_DATA));
969+
verifySchema(result1, schema("@timestamp", "timestamp"));
970+
verifyDataRows(result1, rows("2025-08-01 03:47:41"));
971+
}
961972
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"calcite": {
3+
"logical": "LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(@timestamp=[$0], category=[$1], value=[$2], timestamp=[$3])\n LogicalFilter(condition=[query_string(MAP('query', '(@timestamp:>=1000) AND (@timestamp:<=1754020061123.456)':VARCHAR))])\n CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_time_data]])\n",
4+
"physical": "CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_time_data]], PushDownContext=[[PROJECT->[@timestamp, category, value, timestamp], FILTER->query_string(MAP('query', '(@timestamp:>=1000) AND (@timestamp:<=1754020061123.456)':VARCHAR)), LIMIT->10000], OpenSearchRequestBuilder(sourceBuilder={\"from\":0,\"size\":10000,\"timeout\":\"1m\",\"query\":{\"query_string\":{\"query\":\"(@timestamp:>=1000) AND (@timestamp:<=1754020061123.456)\",\"fields\":[],\"type\":\"best_fields\",\"default_operator\":\"or\",\"max_determinized_states\":10000,\"enable_position_increments\":true,\"fuzziness\":\"AUTO\",\"fuzzy_prefix_length\":0,\"fuzzy_max_expansions\":50,\"phrase_slop\":0,\"escape\":false,\"auto_generate_synonyms_phrase_query\":true,\"fuzzy_transpositions\":true,\"boost\":1.0}},\"_source\":{\"includes\":[\"@timestamp\",\"category\",\"value\",\"timestamp\"],\"excludes\":[]},\"sort\":[{\"_doc\":{\"order\":\"asc\"}}]}, requestedTotalSize=10000, pageSize=null, startFrom=0)])\n"
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"calcite": {
3+
"logical": "LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(@timestamp=[$0], category=[$1], value=[$2], timestamp=[$3])\n LogicalFilter(condition=[query_string(MAP('query', '(@timestamp:>=1000) AND (@timestamp:<=1754020061123.456)':VARCHAR))])\n CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_time_data]])\n",
4+
"physical": "EnumerableLimit(fetch=[10000])\n EnumerableCalc(expr#0..9=[{inputs}], proj#0..3=[{exprs}])\n CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_time_data]], PushDownContext=[[FILTER->query_string(MAP('query', '(@timestamp:>=1000) AND (@timestamp:<=1754020061123.456)':VARCHAR))], OpenSearchRequestBuilder(sourceBuilder={\"from\":0,\"timeout\":\"1m\",\"query\":{\"query_string\":{\"query\":\"(@timestamp:>=1000) AND (@timestamp:<=1754020061123.456)\",\"fields\":[],\"type\":\"best_fields\",\"default_operator\":\"or\",\"max_determinized_states\":10000,\"enable_position_increments\":true,\"fuzziness\":\"AUTO\",\"fuzzy_prefix_length\":0,\"fuzzy_max_expansions\":50,\"phrase_slop\":0,\"escape\":false,\"auto_generate_synonyms_phrase_query\":true,\"fuzzy_transpositions\":true,\"boost\":1.0}},\"sort\":[{\"_doc\":{\"order\":\"asc\"}}]}, requestedTotalSize=2147483647, pageSize=null, startFrom=0)])\n"
5+
}
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"root": {
3+
"name": "ProjectOperator",
4+
"description": {
5+
"fields": "[@timestamp, category, value, timestamp]"
6+
},
7+
"children": [
8+
{
9+
"name": "OpenSearchIndexScan",
10+
"description": {
11+
"request": "OpenSearchQueryRequest(indexName=opensearch-sql_test_index_time_data, sourceBuilder={\"from\":0,\"size\":10000,\"timeout\":\"1m\",\"query\":{\"query_string\":{\"query\":\"(@timestamp:>=1000) AND (@timestamp:<=1754020061123.456)\",\"fields\":[],\"type\":\"best_fields\",\"default_operator\":\"or\",\"max_determinized_states\":10000,\"enable_position_increments\":true,\"fuzziness\":\"AUTO\",\"fuzzy_prefix_length\":0,\"fuzzy_max_expansions\":50,\"phrase_slop\":0,\"escape\":false,\"auto_generate_synonyms_phrase_query\":true,\"fuzzy_transpositions\":true,\"boost\":1.0}},\"_source\":{\"includes\":[\"@timestamp\",\"category\",\"value\",\"timestamp\"],\"excludes\":[]},\"sort\":[{\"_doc\":{\"order\":\"asc\"}}]}, needClean=true, searchDone=false, pitId=*, cursorKeepAlive=1m, searchAfter=null, searchResponse=null)"
12+
},
13+
"children": []
14+
}
15+
]
16+
}
17+
}

ppl/src/main/antlr/OpenSearchPPLParser.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,8 @@ timeModifier
769769
timeModifierValue
770770
: NOW
771771
| NOW LT_PRTHS RT_PRTHS
772+
| DECIMAL_LITERAL
773+
| INTEGER_LITERAL
772774
| stringLiteral
773775
| timeSnap
774776
| (PLUS | MINUS) (integerLiteral)? timeModifierUnit (timeSnap)?

ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -904,19 +904,48 @@ public SearchLiteral visitSearchLiteral(OpenSearchPPLParser.SearchLiteralContext
904904
return new SearchLiteral(new Literal(ctx.getText(), DataType.STRING), false);
905905
}
906906

907+
@Override
908+
public UnresolvedExpression visitTimeModifierValue(
909+
OpenSearchPPLParser.TimeModifierValueContext ctx) {
910+
String osDateMathExpression;
911+
// Convert unix timestamp from seconds to milliseconds for decimal and integer
912+
// as OpenSearch time range accepts unix milliseconds in place of timestamp values
913+
if (ctx.DECIMAL_LITERAL() != null) {
914+
String decimal = ctx.DECIMAL_LITERAL().getText();
915+
BigDecimal unixSecondDecimal = new BigDecimal(decimal);
916+
BigDecimal unixMilliDecimal =
917+
unixSecondDecimal.multiply(BigDecimal.valueOf(1000)).stripTrailingZeros();
918+
osDateMathExpression = unixMilliDecimal.toString();
919+
} else if (ctx.INTEGER_LITERAL() != null) {
920+
String integer = ctx.INTEGER_LITERAL().getText();
921+
osDateMathExpression = String.valueOf(Long.parseLong(integer) * 1000);
922+
} else if (ctx.NOW() != null) { // Converts both NOW and NOW()
923+
// OpenSearch time range accepts "now" as a reference to the current time
924+
osDateMathExpression = ctx.NOW().getText().toLowerCase(Locale.ROOT);
925+
} else {
926+
// Process absolute and relative time modifier values
927+
String pplTimeModifier =
928+
ctx.stringLiteral() != null
929+
? (String) ((Literal) visit(ctx.stringLiteral())).getValue()
930+
: ctx.getText().strip();
931+
// Parse a PPL time modifier to OpenSearch date math expression
932+
osDateMathExpression = DateTimeUtils.parseRelativeTime(pplTimeModifier);
933+
}
934+
return AstDSL.stringLiteral(osDateMathExpression);
935+
}
936+
907937
/**
908938
* Process time range expressions (EARLIEST='value' or LATEST='value') It creates a Comparison
909939
* filter like @timestamp >= timeModifierValue
910940
*/
911941
@Override
912942
public UnresolvedExpression visitTimeModifierExpression(
913943
OpenSearchPPLParser.TimeModifierExpressionContext ctx) {
914-
String pplTimeModifierExpression =
915-
stripSingleQuote(ctx.timeModifier().timeModifierValue().getText().strip());
916944

917-
String osDateMathExpression = DateTimeUtils.parseRelativeTime(pplTimeModifierExpression);
918-
SearchLiteral osDateMathLiteral =
919-
new SearchLiteral(AstDSL.stringLiteral(osDateMathExpression), false);
945+
Literal timeModifierValue =
946+
(Literal) visitTimeModifierValue(ctx.timeModifier().timeModifierValue());
947+
948+
SearchLiteral osDateMathLiteral = new SearchLiteral(timeModifierValue, false);
920949

921950
Field implicitTimestampField =
922951
new Field(new QualifiedName(OpenSearchConstants.IMPLICIT_FIELD_TIMESTAMP), List.of());
@@ -926,11 +955,4 @@ public UnresolvedExpression visitTimeModifierExpression(
926955
: SearchComparison.Operator.LESS_OR_EQUAL;
927956
return new SearchComparison(implicitTimestampField, operator, osDateMathLiteral);
928957
}
929-
930-
private static String stripSingleQuote(String text) {
931-
if (text.length() >= 2 && text.startsWith("'") && text.endsWith("'")) {
932-
return text.substring(1, text.length() - 1);
933-
}
934-
return text;
935-
}
936958
}

ppl/src/test/java/org/opensearch/sql/ppl/parser/AstExpressionBuilderTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,4 +1360,27 @@ public void testMedianAggFuncExpr() {
13601360
emptyList(),
13611361
defaultStatsArgs()));
13621362
}
1363+
1364+
@Test
1365+
public void testTimeModifierEarliestWithNumericValue() {
1366+
assertEqual("source=t earliest=1", search(relation("t"), "@timestamp:>=1000"));
1367+
1368+
assertEqual(
1369+
"source=t earliest=1754020061.123456",
1370+
search(relation("t"), "@timestamp:>=1754020061123.456"));
1371+
}
1372+
1373+
@Test
1374+
public void testTimeModifierLatestWithNowValue() {
1375+
assertEqual(
1376+
"source=t earliest=now latest=now()",
1377+
search(relation("t"), "(@timestamp:>=now) AND (@timestamp:<=now)"));
1378+
}
1379+
1380+
@Test
1381+
public void testTimeModifierEarliestWithStringValue() {
1382+
assertEqual(
1383+
"source=t earliest='2025-12-10 14:00:00'",
1384+
search(relation("t"), "@timestamp:>=2025\\-12\\-10T14\\:00\\:00Z"));
1385+
}
13631386
}

0 commit comments

Comments
 (0)