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 @@ -355,6 +355,28 @@ 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));
return context.relBuilder.peek();
}

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

Version
=======
3.2.0

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


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

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.

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 |
+----------------+-----+
2 changes: 2 additions & 0 deletions docs/user/ppl/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ The query start with search command and then flowing a set of command delimited

- `subquery (aka subsearch) command <cmd/subquery.rst>`_

- `reverse command <cmd/reverse.rst>`_

- `top command <cmd/top.rst>`_

- `trendline command <cmd/trendline.rst>`_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,31 @@ public void testFilterScriptPushDownExplain() throws Exception {
public void testFilterFunctionScriptPushDownExplain() throws Exception {
super.testFilterFunctionScriptPushDownExplain();
}

@Test
public void testExplainWithReverse() throws IOException {
String result =
executeWithReplace(
"explain source=opensearch-sql_test_index_account | sort age | reverse | head 5");

// Verify that the plan contains a LogicalSort with fetch (from head 5)
assertTrue(result.contains("LogicalSort") && result.contains("fetch=[5]"));

// Verify that reverse added a ROW_NUMBER and another sort (descending)
assertTrue(result.contains("ROW_NUMBER()"));
assertTrue(result.contains("dir0=[DESC]"));
}

/**
* Executes the PPL query and returns the result as a string with windows-style line breaks
* replaced with Unix-style ones.
*
* @param ppl the PPL query to execute
* @return the result of the query as a string with line breaks replaced
* @throws IOException if an error occurs during query execution
*/
private String executeWithReplace(String ppl) throws IOException {
var result = executeQueryToString(ppl);
return result.replace("\\r\\n", "\\n");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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));
}

@Test
public void testReverseWithComplexPipeline() throws IOException {
JSONObject result =
executeQuery(
String.format(
"source=%s | where account_number > 18 | fields account_number | reverse | head 2",
TEST_INDEX_BANK));
verifySchema(result, schema("account_number", "bigint"));
verifyDataRowsInOrder(result, rows(32), rows(25));
}

@Test
public void testReverseWithMultipleSorts() throws IOException {
// Use the existing BANK data but with a simpler, more predictable query
JSONObject result =
executeQuery(
String.format(
"source=%s | sort account_number | fields account_number | reverse | head 3",
TEST_INDEX_BANK));
verifySchema(result, schema("account_number", "bigint"));
verifyDataRowsInOrder(result, rows(32), rows(25), rows(20));
}
}
Loading
Loading