Skip to content

Commit 39aecf6

Browse files
committed
udpated implementation: ignore reverse if no collation/@timestamp found
Signed-off-by: Kai Huang <[email protected]>
1 parent fd86fee commit 39aecf6

File tree

14 files changed

+163
-84
lines changed

14 files changed

+163
-84
lines changed

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -669,8 +669,6 @@ public RelNode visitHead(Head node, CalcitePlanContext context) {
669669
return context.relBuilder.peek();
670670
}
671671

672-
private static final String REVERSE_ROW_NUM = "__reverse_row_num__";
673-
674672
@Override
675673
public RelNode visitReverse(
676674
org.opensearch.sql.ast.tree.Reverse node, CalcitePlanContext context) {
@@ -686,17 +684,15 @@ public RelNode visitReverse(
686684
RelCollation reversedCollation = PlanUtils.reverseCollation(collation);
687685
context.relBuilder.sort(reversedCollation);
688686
} else {
689-
// Fallback: use ROW_NUMBER approach when no existing sort
690-
RexNode rowNumber =
691-
context
692-
.relBuilder
693-
.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
694-
.over()
695-
.rowsTo(RexWindowBounds.CURRENT_ROW)
696-
.as(REVERSE_ROW_NUM);
697-
context.relBuilder.projectPlus(rowNumber);
698-
context.relBuilder.sort(context.relBuilder.desc(context.relBuilder.field(REVERSE_ROW_NUM)));
699-
context.relBuilder.projectExcept(context.relBuilder.field(REVERSE_ROW_NUM));
687+
// Check if @timestamp field exists in the row type
688+
List<String> fieldNames = context.relBuilder.peek().getRowType().getFieldNames();
689+
if (fieldNames.contains(OpenSearchConstants.IMPLICIT_FIELD_TIMESTAMP)) {
690+
// If @timestamp exists, sort by it in descending order
691+
context.relBuilder.sort(
692+
context.relBuilder.desc(
693+
context.relBuilder.field(OpenSearchConstants.IMPLICIT_FIELD_TIMESTAMP)));
694+
}
695+
// If neither collation nor @timestamp exists, ignore the reverse command (no-op)
700696
}
701697

702698
return context.relBuilder.peek();

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,11 @@ public void testFilterWithSearchCall() throws IOException {
410410
}
411411

412412
@Test
413-
public void testExplainWithReverse() throws IOException {
413+
public void testExplainWithReverseIgnored() throws IOException {
414+
// Reverse is ignored when there's no existing sort and no @timestamp field
414415
String query = "source=opensearch-sql_test_index_account | reverse | head 5";
415416
var result = explainQueryYaml(query);
416-
String expected = loadExpectedPlan("explain_reverse_fallback.yaml");
417+
String expected = loadExpectedPlan("explain_reverse_ignored.yaml");
417418
assertYamlEqualsIgnoreId(expected, result);
418419
}
419420

@@ -434,10 +435,11 @@ public void testExplainWithReversePushdownMultipleFields() throws IOException {
434435
}
435436

436437
@Test
437-
public void testExplainWithDoubleReverse() throws IOException {
438+
public void testExplainWithDoubleReverseIgnored() throws IOException {
439+
// Double reverse is ignored when there's no existing sort and no @timestamp field
438440
String query = "source=opensearch-sql_test_index_account | reverse | reverse";
439441
var result = explainQueryYaml(query);
440-
String expected = loadExpectedPlan("explain_double_reverse_fallback.yaml");
442+
String expected = loadExpectedPlan("explain_double_reverse_ignored.yaml");
441443
assertYamlEqualsIgnoreId(expected, result);
442444
}
443445

@@ -458,6 +460,15 @@ public void testExplainWithDoubleReversePushdownMultipleFields() throws IOExcept
458460
assertYamlEqualsIgnoreId(expected, result);
459461
}
460462

463+
@Test
464+
public void testExplainReverseWithTimestamp() throws IOException {
465+
// Test that reverse with @timestamp field sorts by @timestamp DESC
466+
String query = "source=opensearch-sql_test_index_time_data | reverse | head 5";
467+
var result = explainQueryYaml(query);
468+
String expected = loadExpectedPlan("explain_reverse_with_timestamp.yaml");
469+
assertYamlEqualsIgnoreId(expected, result);
470+
}
471+
461472
@Test
462473
public void testExplainWithTimechartAvg() throws IOException {
463474
var result = explainQueryYaml("source=events | timechart span=1m avg(cpu_usage) by host");

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

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.opensearch.sql.calcite.remote;
77

88
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK;
9+
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_TIME_DATA;
910
import static org.opensearch.sql.util.MatcherUtils.rows;
1011
import static org.opensearch.sql.util.MatcherUtils.schema;
1112
import static org.opensearch.sql.util.MatcherUtils.verifyDataRowsInOrder;
@@ -24,12 +25,16 @@ public void init() throws Exception {
2425
enableCalcite();
2526
disallowCalciteFallback();
2627
loadIndex(Index.BANK);
28+
loadIndex(Index.TIME_TEST_DATA);
2729
}
2830

2931
@Test
3032
public void testReverse() throws IOException {
3133
JSONObject result =
32-
executeQuery(String.format("source=%s | fields account_number | reverse", TEST_INDEX_BANK));
34+
executeQuery(
35+
String.format(
36+
"source=%s | fields account_number | sort account_number | reverse",
37+
TEST_INDEX_BANK));
3338
verifySchema(result, schema("account_number", "bigint"));
3439
verifyDataRowsInOrder(
3540
result, rows(32), rows(25), rows(20), rows(18), rows(13), rows(6), rows(1));
@@ -40,7 +45,8 @@ public void testReverseWithFields() throws IOException {
4045
JSONObject result =
4146
executeQuery(
4247
String.format(
43-
"source=%s | fields account_number, firstname | reverse", TEST_INDEX_BANK));
48+
"source=%s | fields account_number, firstname | sort account_number | reverse",
49+
TEST_INDEX_BANK));
4450
verifySchema(result, schema("account_number", "bigint"), schema("firstname", "string"));
4551
verifyDataRowsInOrder(
4652
result,
@@ -70,7 +76,8 @@ public void testDoubleReverse() throws IOException {
7076
JSONObject result =
7177
executeQuery(
7278
String.format(
73-
"source=%s | fields account_number | reverse | reverse", TEST_INDEX_BANK));
79+
"source=%s | fields account_number | sort account_number | reverse | reverse",
80+
TEST_INDEX_BANK));
7481
verifySchema(result, schema("account_number", "bigint"));
7582
verifyDataRowsInOrder(
7683
result, rows(1), rows(6), rows(13), rows(18), rows(20), rows(25), rows(32));
@@ -80,7 +87,9 @@ public void testDoubleReverse() throws IOException {
8087
public void testReverseWithHead() throws IOException {
8188
JSONObject result =
8289
executeQuery(
83-
String.format("source=%s | fields account_number | reverse | head 3", TEST_INDEX_BANK));
90+
String.format(
91+
"source=%s | fields account_number | sort account_number | reverse | head 3",
92+
TEST_INDEX_BANK));
8493
verifySchema(result, schema("account_number", "bigint"));
8594
verifyDataRowsInOrder(result, rows(32), rows(25), rows(20));
8695
}
@@ -90,7 +99,8 @@ public void testReverseWithComplexPipeline() throws IOException {
9099
JSONObject result =
91100
executeQuery(
92101
String.format(
93-
"source=%s | where account_number > 18 | fields account_number | reverse | head 2",
102+
"source=%s | where account_number > 18 | fields account_number | sort"
103+
+ " account_number | reverse | head 2",
94104
TEST_INDEX_BANK));
95105
verifySchema(result, schema("account_number", "bigint"));
96106
verifyDataRowsInOrder(result, rows(32), rows(25));
@@ -163,4 +173,55 @@ public void testDoubleReverseWithMixedSortDirections() throws IOException {
163173
rows(6, "Hattie"),
164174
rows(1, "Amber JOHnny"));
165175
}
176+
177+
@Test
178+
public void testReverseIgnoredWithoutSortOrTimestamp() throws IOException {
179+
// Test that reverse is ignored when there's no explicit sort and no @timestamp field
180+
// BANK index doesn't have @timestamp, so reverse should be ignored
181+
JSONObject result =
182+
executeQuery(
183+
String.format("source=%s | fields account_number | reverse | head 3", TEST_INDEX_BANK));
184+
verifySchema(result, schema("account_number", "bigint"));
185+
// Without sort or @timestamp, reverse is ignored, so data comes in natural order
186+
// The first 3 documents in natural order (ascending by account_number)
187+
verifyDataRowsInOrder(result, rows(1), rows(6), rows(13));
188+
}
189+
190+
@Test
191+
public void testReverseWithTimestampField() throws IOException {
192+
// Test that reverse with @timestamp field sorts by @timestamp DESC
193+
// TIME_TEST_DATA index has @timestamp field
194+
JSONObject result =
195+
executeQuery(
196+
String.format(
197+
"source=%s | fields value, category, `@timestamp` | reverse | head 5",
198+
TEST_INDEX_TIME_DATA));
199+
verifySchema(
200+
result,
201+
schema("value", "int"),
202+
schema("category", "string"),
203+
schema("@timestamp", "timestamp"));
204+
// Should return the latest 5 records (highest @timestamp values) in descending order
205+
// Based on the test data, these are IDs 100, 99, 98, 97, 96
206+
verifyDataRowsInOrder(
207+
result,
208+
rows(8762, "A", "2025-08-01 03:47:41"),
209+
rows(7348, "C", "2025-08-01 02:00:56"),
210+
rows(9015, "B", "2025-08-01 01:14:11"),
211+
rows(6489, "D", "2025-08-01 00:27:26"),
212+
rows(8676, "A", "2025-07-31 23:40:33"));
213+
}
214+
215+
@Test
216+
public void testReverseWithTimestampAndExplicitSort() throws IOException {
217+
// Test that explicit sort takes precedence over @timestamp
218+
JSONObject result =
219+
executeQuery(
220+
String.format(
221+
"source=%s | fields value, category | sort value | reverse | head 3",
222+
TEST_INDEX_TIME_DATA));
223+
verifySchema(result, schema("value", "int"), schema("category", "string"));
224+
// Should reverse the value sort, giving us the highest values
225+
verifyDataRowsInOrder(result, rows(9521, "B"), rows(9367, "A"), rows(9321, "A"));
226+
}
166227
}

integ-test/src/test/resources/expectedOutput/calcite/explain_double_reverse_fallback.yaml

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
calcite:
2+
logical: |
3+
LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])
4+
LogicalProject(account_number=[$0], firstname=[$1], address=[$2], balance=[$3], gender=[$4], city=[$5], employer=[$6], state=[$7], age=[$8], email=[$9], lastname=[$10])
5+
CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]])
6+
physical: |
7+
CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]], PushDownContext=[[PROJECT->[account_number, firstname, address, balance, gender, city, employer, state, age, email, lastname], LIMIT->10000], OpenSearchRequestBuilder(sourceBuilder={"from":0,"size":10000,"timeout":"1m","_source":{"includes":["account_number","firstname","address","balance","gender","city","employer","state","age","email","lastname"],"excludes":[]}}, requestedTotalSize=10000, pageSize=null, startFrom=0)])

integ-test/src/test/resources/expectedOutput/calcite/explain_reverse_fallback.yaml

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
calcite:
2+
logical: |
3+
LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])
4+
LogicalProject(account_number=[$0], firstname=[$1], address=[$2], balance=[$3], gender=[$4], city=[$5], employer=[$6], state=[$7], age=[$8], email=[$9], lastname=[$10])
5+
LogicalSort(fetch=[5])
6+
CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]])
7+
physical: |
8+
CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]], PushDownContext=[[PROJECT->[account_number, firstname, address, balance, gender, city, employer, state, age, email, lastname], LIMIT->5, LIMIT->10000], OpenSearchRequestBuilder(sourceBuilder={"from":0,"size":5,"timeout":"1m","_source":{"includes":["account_number","firstname","address","balance","gender","city","employer","state","age","email","lastname"],"excludes":[]}}, requestedTotalSize=5, pageSize=null, startFrom=0)])
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
calcite:
2+
logical: |
3+
LogicalSystemLimit(sort0=[$0], dir0=[DESC], fetch=[10000], type=[QUERY_SIZE_LIMIT])
4+
LogicalProject(@timestamp=[$0], category=[$1], value=[$2], timestamp=[$3])
5+
LogicalSort(sort0=[$0], dir0=[DESC], fetch=[5])
6+
CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_time_data]])
7+
physical: |
8+
CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_time_data]], PushDownContext=[[PROJECT->[@timestamp, category, value, timestamp], SORT->[{
9+
"@timestamp" : {
10+
"order" : "desc",
11+
"missing" : "_first"
12+
}
13+
}], LIMIT->5, LIMIT->10000], OpenSearchRequestBuilder(sourceBuilder={"from":0,"size":5,"timeout":"1m","_source":{"includes":["@timestamp","category","value","timestamp"],"excludes":[]},"sort":[{"@timestamp":{"order":"desc","missing":"_first"}}]}, requestedTotalSize=5, pageSize=null, startFrom=0)])

integ-test/src/test/resources/expectedOutput/calcite_no_pushdown/explain_double_reverse_fallback.yaml

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
calcite:
2+
logical: |
3+
LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])
4+
LogicalProject(account_number=[$0], firstname=[$1], address=[$2], balance=[$3], gender=[$4], city=[$5], employer=[$6], state=[$7], age=[$8], email=[$9], lastname=[$10])
5+
CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]])
6+
physical: |
7+
EnumerableLimit(fetch=[10000])
8+
EnumerableCalc(expr#0..16=[{inputs}], proj#0..10=[{exprs}])
9+
CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_account]])

0 commit comments

Comments
 (0)