Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.ViewExpanders;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Aggregate;
import org.apache.calcite.rel.core.JoinRelType;
Expand Down Expand Up @@ -668,25 +669,32 @@ public RelNode visitHead(Head node, CalcitePlanContext context) {
return context.relBuilder.peek();
}

private static final String REVERSE_ROW_NUM = "__reverse_row_num__";

@Override
public RelNode visitReverse(
org.opensearch.sql.ast.tree.Reverse node, CalcitePlanContext context) {
visitChildren(node, context);
// Add ROW_NUMBER() column
RexNode rowNumber =
context
.relBuilder
.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
.over()
.rowsTo(RexWindowBounds.CURRENT_ROW)
.as(REVERSE_ROW_NUM);
context.relBuilder.projectPlus(rowNumber);
// Sort by row number descending
context.relBuilder.sort(context.relBuilder.desc(context.relBuilder.field(REVERSE_ROW_NUM)));
// Remove row number column
context.relBuilder.projectExcept(context.relBuilder.field(REVERSE_ROW_NUM));

// Check if there's an existing sort to reverse
List<RelCollation> collations =
context.relBuilder.getCluster().getMetadataQuery().collations(context.relBuilder.peek());
RelCollation collation = collations != null && !collations.isEmpty() ? collations.get(0) : null;

if (collation != null && !collation.getFieldCollations().isEmpty()) {
// If there's an existing sort, reverse its direction
RelCollation reversedCollation = PlanUtils.reverseCollation(collation);
context.relBuilder.sort(reversedCollation);
} else {
// Check if @timestamp field exists in the row type
List<String> fieldNames = context.relBuilder.peek().getRowType().getFieldNames();
if (fieldNames.contains(OpenSearchConstants.IMPLICIT_FIELD_TIMESTAMP)) {
// If @timestamp exists, sort by it in descending order
context.relBuilder.sort(
context.relBuilder.desc(
context.relBuilder.field(OpenSearchConstants.IMPLICIT_FIELD_TIMESTAMP)));
}
// If neither collation nor @timestamp exists, ignore the reverse command (no-op)
}

return context.relBuilder.peek();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelFieldCollation;
import org.apache.calcite.rel.RelHomogeneousShuttle;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelShuttle;
Expand Down Expand Up @@ -568,6 +571,37 @@ public Void visitCorrelVariable(RexCorrelVariable correlVar) {
}
}

/**
* Reverses the direction of a RelCollation.
*
* @param original The original collation to reverse
* @return A new RelCollation with reversed directions
*/
public static RelCollation reverseCollation(RelCollation original) {
if (original == null || original.getFieldCollations().isEmpty()) {
return original;
}

List<RelFieldCollation> reversedFields = new ArrayList<>();
for (RelFieldCollation field : original.getFieldCollations()) {
RelFieldCollation.Direction reversedDirection = field.direction.reverse();

// Handle null direction properly - reverse it as well
RelFieldCollation.NullDirection reversedNullDirection =
field.nullDirection == RelFieldCollation.NullDirection.FIRST
? RelFieldCollation.NullDirection.LAST
: field.nullDirection == RelFieldCollation.NullDirection.LAST
? RelFieldCollation.NullDirection.FIRST
: field.nullDirection;

RelFieldCollation reversedField =
new RelFieldCollation(field.getFieldIndex(), reversedDirection, reversedNullDirection);
reversedFields.add(reversedField);
}

return RelCollations.of(reversedFields);
}

/** Adds a rel node to the top of the stack while preserving the field names and aliases. */
static void replaceTop(RelBuilder relBuilder, RelNode relNode) {
try {
Expand Down
1 change: 1 addition & 0 deletions docs/category.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"user/ppl/cmd/rename.rst",
"user/ppl/cmd/multisearch.rst",
"user/ppl/cmd/replace.rst",
"user/ppl/cmd/reverse.rst",
"user/ppl/cmd/rex.rst",
"user/ppl/cmd/search.rst",
"user/ppl/cmd/showdatasources.rst",
Expand Down
90 changes: 63 additions & 27 deletions docs/user/ppl/cmd/reverse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,21 @@ reverse

Description
============
| Using ``reverse`` command to reverse the display order of search results. The same results are returned, but in reverse order.
| Using ``reverse`` command to reverse the display order of search results. The behavior depends on the query context:
|
| **1. With existing sort**: Reverses the sort direction(s)
| **2. With @timestamp field (no explicit sort)**: Sorts by @timestamp in descending order
| **3. Without sort or @timestamp**: The command is ignored (no effect)

Behavior
========
The ``reverse`` command follows a three-tier logic:

1. **If there's an explicit sort command before reverse**: The reverse command flips all sort directions (ASC ↔ DESC)
2. **If no explicit sort but the index has an @timestamp field**: The reverse command sorts by @timestamp in descending order (most recent first)
3. **If neither condition is met**: The reverse command is ignored and has no effect on the result order

This design optimizes performance by avoiding expensive operations when reverse has no meaningful semantic interpretation.

Version
=======
Expand All @@ -26,16 +40,16 @@ reverse

Note
=====
The `reverse` command processes the entire dataset. If applied directly to millions of records, it will consume significant memory resources on the coordinating node. Users should only apply the `reverse` command to smaller datasets, typically after aggregation operations.
The ``reverse`` command is optimized to avoid unnecessary memory consumption. When applied without an explicit sort or @timestamp field, it is ignored. When used with an explicit sort, it efficiently reverses the sort direction(s) without materializing the entire dataset.

Example 1: Basic reverse operation
==================================
Example 1: Reverse with explicit sort
======================================

The example shows reversing the order of all documents.
The example shows reversing results after sorting by age in ascending order, effectively giving descending order.

PPL query::

os> source=accounts | fields account_number, age | reverse;
os> source=accounts | sort age | fields account_number, age | reverse;
fetched rows / total rows = 4/4
+----------------+-----+
| account_number | age |
Expand All @@ -47,33 +61,52 @@ PPL query::
+----------------+-----+


Example 2: Reverse with sort
============================
Example 2: Reverse with @timestamp field
=========================================

The example shows reversing results after sorting by age in ascending order, effectively giving descending order.
The example shows reverse on a time-series index automatically sorts by @timestamp in descending order (most recent first).

PPL query::

os> source=accounts | sort age | fields account_number, age | reverse;
fetched rows / total rows = 4/4
os> source=time_test | fields value, @timestamp | reverse | head 3;
fetched rows / total rows = 3/3
+-------+---------------------+
| value | @timestamp |
|-------+---------------------|
| 9243 | 2025-07-28 09:41:29 |
| 7654 | 2025-07-28 08:22:11 |
| 8321 | 2025-07-28 07:05:33 |
+-------+---------------------+

Note: When the index contains an @timestamp field and no explicit sort is specified, reverse will sort by @timestamp DESC to show the most recent events first. This is particularly useful for log analysis and time-series data.

Example 3: Reverse ignored (no sort, no @timestamp)
===================================================

The example shows that reverse is ignored when there's no explicit sort and no @timestamp field.

PPL query::

os> source=accounts | fields account_number, age | reverse | head 2;
fetched rows / total rows = 2/2
+----------------+-----+
| account_number | age |
|----------------+-----|
| 6 | 36 |
| 18 | 33 |
| 1 | 32 |
| 13 | 28 |
| 6 | 36 |
+----------------+-----+

Note: Results appear in natural order (same as without reverse) because accounts index has no @timestamp field and no explicit sort was specified.


Example 3: Reverse with head
============================
Example 4: Reverse with sort and head
=====================================

The example shows using reverse with head to get the last 2 records from the original order.
The example shows using reverse with sort and head to get the top 2 records by age.

PPL query::

os> source=accounts | reverse | head 2 | fields account_number, age;
os> source=accounts | sort age | reverse | head 2 | fields account_number, age;
fetched rows / total rows = 2/2
+----------------+-----+
| account_number | age |
Expand All @@ -83,14 +116,14 @@ PPL query::
+----------------+-----+


Example 4: Double reverse
=========================
Example 5: Double reverse with sort
===================================

The example shows that applying reverse twice returns to the original order.
The example shows that applying reverse twice with an explicit sort returns to the original sort order.

PPL query::

os> source=accounts | reverse | reverse | fields account_number, age;
os> source=accounts | sort age | reverse | reverse | fields account_number, age;
fetched rows / total rows = 4/4
+----------------+-----+
| account_number | age |
Expand All @@ -102,19 +135,22 @@ PPL query::
+----------------+-----+


Example 5: Reverse with complex pipeline
=======================================
Example 6: Reverse with multiple sort fields
============================================

The example shows reverse working with filtering and field selection.
The example shows reverse flipping all sort directions when multiple fields are sorted.

PPL query::

os> source=accounts | where age > 30 | fields account_number, age | reverse;
fetched rows / total rows = 3/3
os> source=accounts | sort +age, -account_number | reverse | fields account_number, age;
fetched rows / total rows = 4/4
+----------------+-----+
| account_number | age |
|----------------+-----|
| 6 | 36 |
| 18 | 33 |
| 1 | 32 |
| 13 | 28 |
+----------------+-----+

Note: Original sort is ASC age, DESC account_number. After reverse, it becomes DESC age, ASC account_number.
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
CalciteQueryAnalysisIT.class,
CalciteRareCommandIT.class,
CalciteRegexCommandIT.class,
CalciteReverseCommandIT.class,
CalciteRexCommandIT.class,
CalciteRenameCommandIT.class,
CalciteReplaceCommandIT.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,17 +410,63 @@ public void testFilterWithSearchCall() throws IOException {
}

@Test
public void testExplainWithReverse() throws IOException {
String result =
executeWithReplace(
"explain source=opensearch-sql_test_index_account | sort age | reverse | head 5");
public void testExplainWithReverseIgnored() throws IOException {
// Reverse is ignored when there's no existing sort and no @timestamp field
String query = "source=opensearch-sql_test_index_account | reverse | head 5";
var result = explainQueryYaml(query);
String expected = loadExpectedPlan("explain_reverse_ignored.yaml");
assertYamlEqualsIgnoreId(expected, result);
}

@Test
public void testExplainWithReversePushdown() throws IOException {
String query = "source=opensearch-sql_test_index_account | sort - age | reverse";
var result = explainQueryYaml(query);
String expected = loadExpectedPlan("explain_reverse_pushdown_single.yaml");
assertYamlEqualsIgnoreId(expected, result);
}

@Test
public void testExplainWithReversePushdownMultipleFields() throws IOException {
String query = "source=opensearch-sql_test_index_account | sort - age, + firstname | reverse";
var result = explainQueryYaml(query);
String expected = loadExpectedPlan("explain_reverse_pushdown_multiple.yaml");
assertYamlEqualsIgnoreId(expected, result);
}

@Test
public void testExplainWithDoubleReverseIgnored() throws IOException {
// Double reverse is ignored when there's no existing sort and no @timestamp field
String query = "source=opensearch-sql_test_index_account | reverse | reverse";
var result = explainQueryYaml(query);
String expected = loadExpectedPlan("explain_double_reverse_ignored.yaml");
assertYamlEqualsIgnoreId(expected, result);
}

@Test
public void testExplainWithDoubleReversePushdown() throws IOException {
String query = "source=opensearch-sql_test_index_account | sort - age | reverse | reverse";
var result = explainQueryYaml(query);
String expected = loadExpectedPlan("explain_double_reverse_pushdown_single.yaml");
assertYamlEqualsIgnoreId(expected, result);
}

// Verify that the plan contains a LogicalSort with fetch (from head 5)
assertTrue(result.contains("LogicalSort") && result.contains("fetch=[5]"));
@Test
public void testExplainWithDoubleReversePushdownMultipleFields() throws IOException {
String query =
"source=opensearch-sql_test_index_account | sort - age, + firstname | reverse | reverse";
var result = explainQueryYaml(query);
String expected = loadExpectedPlan("explain_double_reverse_pushdown_multiple.yaml");
assertYamlEqualsIgnoreId(expected, result);
}

// Verify that reverse added a ROW_NUMBER and another sort (descending)
assertTrue(result.contains("ROW_NUMBER()"));
assertTrue(result.contains("dir0=[DESC]"));
@Test
public void testExplainReverseWithTimestamp() throws IOException {
// Test that reverse with @timestamp field sorts by @timestamp DESC
String query = "source=opensearch-sql_test_index_time_data | reverse | head 5";
var result = explainQueryYaml(query);
String expected = loadExpectedPlan("explain_reverse_with_timestamp.yaml");
assertYamlEqualsIgnoreId(expected, result);
}

@Test
Expand Down
Loading
Loading