Skip to content
Closed
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 @@ -80,6 +80,7 @@
import org.opensearch.sql.ast.tree.Relation;
import org.opensearch.sql.ast.tree.RelationSubquery;
import org.opensearch.sql.ast.tree.Rename;
import org.opensearch.sql.ast.tree.Reverse;
import org.opensearch.sql.ast.tree.Sort;
import org.opensearch.sql.ast.tree.Sort.SortOption;
import org.opensearch.sql.ast.tree.SubqueryAlias;
Expand Down Expand Up @@ -682,6 +683,12 @@ public LogicalPlan visitFlatten(Flatten node, AnalysisContext context) {
"FLATTEN is supported only when " + CALCITE_ENGINE_ENABLED.getKeyValue() + "=true");
}

@Override
public LogicalPlan visitReverse(Reverse node, AnalysisContext context) {
throw new UnsupportedOperationException(
"REVERSE is supported only when " + CALCITE_ENGINE_ENABLED.getKeyValue() + "=true");
}

@Override
public LogicalPlan visitPaginate(Paginate paginate, AnalysisContext context) {
LogicalPlan child = paginate.getChild().get(0).accept(this, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import org.opensearch.sql.ast.tree.Relation;
import org.opensearch.sql.ast.tree.RelationSubquery;
import org.opensearch.sql.ast.tree.Rename;
import org.opensearch.sql.ast.tree.Reverse;
import org.opensearch.sql.ast.tree.Sort;
import org.opensearch.sql.ast.tree.SubqueryAlias;
import org.opensearch.sql.ast.tree.TableFunction;
Expand Down Expand Up @@ -244,6 +245,10 @@ public T visitSort(Sort node, C context) {
return visitChildren(node, context);
}

public T visitReverse(Reverse node, C context) {
return visitChildren(node, context);
}

public T visitLambdaFunction(LambdaFunction node, C context) {
return visitChildren(node, context);
}
Expand Down
42 changes: 42 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/tree/Reverse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.ast.tree;

import com.google.common.collect.ImmutableList;
import java.util.List;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.opensearch.sql.ast.AbstractNodeVisitor;

/** AST node represent Reverse operation. */
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = false)
@RequiredArgsConstructor
public class Reverse extends UnresolvedPlan {

private UnresolvedPlan child;

@Override
public Reverse attach(UnresolvedPlan child) {
this.child = child;
return this;
}

@Override
public List<UnresolvedPlan> getChild() {
return this.child == null ? ImmutableList.of() : ImmutableList.of(this.child);
}

@Override
public <T, C> T accept(AbstractNodeVisitor<T, C> nodeVisitor, C context) {
return nodeVisitor.visitReverse(this, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import org.opensearch.sql.ast.tree.RareTopN;
import org.opensearch.sql.ast.tree.Relation;
import org.opensearch.sql.ast.tree.Rename;
import org.opensearch.sql.ast.tree.Reverse;
import org.opensearch.sql.ast.tree.Sort;
import org.opensearch.sql.ast.tree.Sort.SortOption;
import org.opensearch.sql.ast.tree.SubqueryAlias;
Expand Down Expand Up @@ -355,6 +356,20 @@ public RelNode visitHead(Head node, CalcitePlanContext context) {
return context.relBuilder.peek();
}

@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__"));
Comment on lines +364 to +369

Copilot AI Jul 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alias 'reverse_row_num' is used as a magic string. Extract this into a shared constant to improve maintainability and avoid typos when referencing it in multiple places.

Suggested change
.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__"));
.over().rowsTo(RexWindowBounds.CURRENT_ROW).as(REVERSE_ROW_NUM_ALIAS);
context.relBuilder.projectPlus(rowNumber);
// Sort by row number descending
context.relBuilder.sort(context.relBuilder.desc(context.relBuilder.field(REVERSE_ROW_NUM_ALIAS)));
// Remove row number column
context.relBuilder.projectExcept(context.relBuilder.field(REVERSE_ROW_NUM_ALIAS));

Copilot uses AI. Check for mistakes.
return context.relBuilder.peek();
}

@Override
public RelNode visitParse(Parse node, CalcitePlanContext context) {
visitChildren(node, context);
Expand Down
114 changes: 114 additions & 0 deletions docs/user/ppl/cmd/reverse.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
=============
reverse
=============

.. rubric:: Table of contents

.. contents::
:local:
:depth: 2


Description
============
| Using ``reverse`` command to reverse the display order of search results. The same results are returned, but in reverse order.


Syntax
============
reverse


* No parameters: The reverse command takes no arguments or options.


Example 1: Basic reverse operation
==================================

The example shows reversing the order of all documents.

PPL query::

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


Example 2: Reverse with sort
============================

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

PPL query::

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


Example 3: Reverse with head
============================

The example shows using reverse with head to get the last 2 records from the original order.

PPL query::

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


Example 4: Double reverse
=========================

The example shows that applying reverse twice returns to the original order.

PPL query::

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


Example 5: Reverse with complex pipeline
=======================================

The example shows reverse working with filtering and field selection.

PPL query::

os> source=accounts | where age > 30 | fields account_number, age | reverse;
fetched rows / total rows = 3/3
+----------------+-----+
| account_number | age |
|----------------+-----|
| 6 | 36 |
| 18 | 33 |
| 1 | 32 |
+----------------+-----+
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite.remote;

import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK;
import static org.opensearch.sql.util.MatcherUtils.rows;
import static org.opensearch.sql.util.MatcherUtils.schema;
import static org.opensearch.sql.util.MatcherUtils.verifyDataRowsInOrder;
import static org.opensearch.sql.util.MatcherUtils.verifySchema;

import java.io.IOException;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.opensearch.sql.ppl.PPLIntegTestCase;

public class CalciteReverseCommandIT extends PPLIntegTestCase {

@Override
public void init() throws Exception {
super.init();
enableCalcite();
disallowCalciteFallback();
loadIndex(Index.BANK);
}

@Test
public void testReverse() throws IOException {
JSONObject result = executeQuery(String.format("source=%s | fields account_number | reverse", TEST_INDEX_BANK));
verifySchema(result, schema("account_number", "bigint"));
verifyDataRowsInOrder(result, rows(32), rows(25), rows(20), rows(18), rows(13), rows(6), rows(1));
}

@Test
public void testReverseWithFields() throws IOException {
JSONObject result = executeQuery(String.format("source=%s | fields account_number, firstname | reverse", TEST_INDEX_BANK));
verifySchema(result, schema("account_number", "bigint"), schema("firstname", "string"));
verifyDataRowsInOrder(
result,
rows(32, "Dillard"),
rows(25, "Virginia"),
rows(20, "Elinor"),
rows(18, "Dale"),
rows(13, "Nanette"),
rows(6, "Hattie"),
rows(1, "Amber JOHnny"));
}

@Test
public void testReverseWithSort() throws IOException {
JSONObject result = executeQuery(String.format("source=%s | sort account_number | fields account_number | reverse", TEST_INDEX_BANK));
verifySchema(result, schema("account_number", "bigint"));
verifyDataRowsInOrder(result, rows(32), rows(25), rows(20), rows(18), rows(13), rows(6), rows(1));
}

@Test
public void testDoubleReverse() throws IOException {
JSONObject result = executeQuery(String.format("source=%s | fields account_number | reverse | reverse", TEST_INDEX_BANK));
verifySchema(result, schema("account_number", "bigint"));
verifyDataRowsInOrder(result, rows(1), rows(6), rows(13), rows(18), rows(20), rows(25), rows(32));
}

@Test
public void testReverseWithHead() throws IOException {
JSONObject result = executeQuery(String.format("source=%s | fields account_number | reverse | head 3", TEST_INDEX_BANK));
verifySchema(result, schema("account_number", "bigint"));
verifyDataRowsInOrder(result, rows(32), rows(25), rows(20));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public void onMatch(RelOptRuleCall call) {

Integer limitValue = extractLimitValue(sort.fetch);
Integer offsetValue = extractOffsetValue(sort.offset);
// Skip this rule if this is a reverse operation (indicated by row_number)
if (hasRowNumberFunction(sort)) {
return;
}
if (limitValue != null && offsetValue != null) {
CalciteLogicalIndexScan newScan = scan.pushDownLimit(limitValue, offsetValue);
if (newScan != null) {
Expand All @@ -40,6 +44,18 @@ public void onMatch(RelOptRuleCall call) {
}
}

/**
* Check if the LogicalSort contains a row_number function, which indicates a reverse operation.
*
* @param sort The LogicalSort to check
* @return True if a row_number function is found, false otherwise
*/
private boolean hasRowNumberFunction(LogicalSort sort) {
// Check if the sort has a row_number function in its digest
String digest = sort.getDigest();
return digest != null && digest.contains("row_number");
Comment on lines +54 to +56

Copilot AI Jul 17, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation of hasRowNumberFunction relies on searching for 'row_number' in the sort digest, which is brittle. Consider inspecting the sort node's collation fields or checking for a preceding Project that introduces the 'reverse_row_num' column instead of parsing the digest string.

Suggested change
// Check if the sort has a row_number function in its digest
String digest = sort.getDigest();
return digest != null && digest.contains("row_number");
// Check collation fields for row_number function
if (sort.getCollation() != null) {
return sort.getCollation().getFieldCollations().stream()
.anyMatch(fieldCollation -> fieldCollation.getFieldName().equalsIgnoreCase("row_number"));
}
// Check for preceding Project node introducing __reverse_row_num__ column
if (sort.getInput() instanceof LogicalProject) {
LogicalProject project = (LogicalProject) sort.getInput();
return project.getProjects().stream()
.anyMatch(rexNode -> rexNode.toString().contains("__reverse_row_num__"));
}
return false;

Copilot uses AI. Check for mistakes.
}

private static Integer extractLimitValue(RexNode fetch) {
// fetch is always a integer literal (specified in our PPL/SQL syntax)
if (fetch instanceof RexLiteral) {
Expand Down
6 changes: 6 additions & 0 deletions ppl/src/main/antlr/OpenSearchPPLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ commands
| appendcolCommand
| expandCommand
| flattenCommand
| reverseCommand
;

commandName
Expand Down Expand Up @@ -103,6 +104,7 @@ commandName
| FLATTEN
| TRENDLINE
| EXPLAIN
| REVERSE
;

searchCommand
Expand Down Expand Up @@ -147,6 +149,10 @@ sortCommand
: SORT sortbyClause
;

reverseCommand
: REVERSE
;

evalCommand
: EVAL evalClause (COMMA evalClause)*
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import org.opensearch.sql.ast.tree.RareTopN.CommandType;
import org.opensearch.sql.ast.tree.Relation;
import org.opensearch.sql.ast.tree.Rename;
import org.opensearch.sql.ast.tree.Reverse;
import org.opensearch.sql.ast.tree.Sort;
import org.opensearch.sql.ast.tree.SubqueryAlias;
import org.opensearch.sql.ast.tree.TableFunction;
Expand Down Expand Up @@ -379,6 +380,12 @@ public UnresolvedPlan visitSortCommand(SortCommandContext ctx) {
.collect(Collectors.toList()));
}

/** Reverse command. */
@Override
public UnresolvedPlan visitReverseCommand(OpenSearchPPLParser.ReverseCommandContext ctx) {
return new Reverse();
}

/** Eval command. */
@Override
public UnresolvedPlan visitEvalCommand(EvalCommandContext ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import org.opensearch.sql.ast.tree.RareTopN;
import org.opensearch.sql.ast.tree.Relation;
import org.opensearch.sql.ast.tree.Rename;
import org.opensearch.sql.ast.tree.Reverse;
import org.opensearch.sql.ast.tree.Sort;
import org.opensearch.sql.ast.tree.SubqueryAlias;
import org.opensearch.sql.ast.tree.TableFunction;
Expand Down Expand Up @@ -351,6 +352,12 @@ public String visitHead(Head node, String context) {
return StringUtils.format("%s | head %d", child, size);
}

@Override
public String visitReverse(Reverse node, String context) {
String child = node.getChild().get(0).accept(this, context);
return StringUtils.format("%s | reverse", child);
}

@Override
public String visitParse(Parse node, String context) {
String child = node.getChild().get(0).accept(this, context);
Expand Down
Loading
Loading