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 @@ -99,6 +99,7 @@
import org.opensearch.sql.ast.tree.StreamWindow;
import org.opensearch.sql.ast.tree.SubqueryAlias;
import org.opensearch.sql.ast.tree.TableFunction;
import org.opensearch.sql.ast.tree.Transpose;
import org.opensearch.sql.ast.tree.Trendline;
import org.opensearch.sql.ast.tree.UnresolvedPlan;
import org.opensearch.sql.ast.tree.Values;
Expand Down Expand Up @@ -703,6 +704,11 @@ public LogicalPlan visitML(ML node, AnalysisContext context) {
return new LogicalML(child, node.getArguments());
}

@Override
public LogicalPlan visitTranspose(Transpose node, AnalysisContext context) {
throw getOnlyForCalciteException("Transpose");
}

@Override
public LogicalPlan visitBin(Bin node, AnalysisContext context) {
throw getOnlyForCalciteException("Bin");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import org.opensearch.sql.ast.tree.StreamWindow;
import org.opensearch.sql.ast.tree.SubqueryAlias;
import org.opensearch.sql.ast.tree.TableFunction;
import org.opensearch.sql.ast.tree.Transpose;
import org.opensearch.sql.ast.tree.Trendline;
import org.opensearch.sql.ast.tree.Values;
import org.opensearch.sql.ast.tree.Window;
Expand Down Expand Up @@ -282,6 +283,10 @@ public T visitReverse(Reverse node, C context) {
return visitChildren(node, context);
}

public T visitTranspose(Transpose node, C context) {
return visitChildren(node, context);
}

public T visitChart(Chart node, C context) {
return visitChildren(node, context);
}
Expand Down
66 changes: 66 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/tree/Transpose.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.*;
import org.opensearch.sql.ast.AbstractNodeVisitor;
import org.opensearch.sql.ast.expression.Argument;
import org.opensearch.sql.common.utils.StringUtils;

/** AST node represent Transpose operation. */
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = false)
@RequiredArgsConstructor
public class Transpose extends UnresolvedPlan {
private final @NonNull java.util.Map<String, Argument> arguments;
private UnresolvedPlan child;
private static final int MAX_LIMIT_TRANSPOSE = 10000;

public Integer getMaxRows() {
Integer maxRows = 5;
if (arguments.containsKey("number") && arguments.get("number").getValue() != null) {
try {
maxRows = Integer.parseInt(arguments.get("number").getValue().toString());
} catch (NumberFormatException e) {
// log warning and use default
maxRows = 5;
}
}
if (maxRows > MAX_LIMIT_TRANSPOSE) {
throw new IllegalArgumentException(
StringUtils.format("Maximum limit to transpose is %s", MAX_LIMIT_TRANSPOSE));
}
return maxRows;
}

public String getColumnName() {
String columnName = "column";
if (arguments.containsKey("columnName") && arguments.get("columnName").getValue() != null) {
columnName = arguments.get("columnName").getValue().toString();
}
return columnName;
}

@Override
public Transpose 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.visitTranspose(this, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFamily;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexCorrelVariable;
import org.apache.calcite.rex.RexInputRef;
Expand Down Expand Up @@ -696,6 +697,64 @@ public RelNode visitReverse(
return context.relBuilder.peek();
}

@Override
public RelNode visitTranspose(
org.opensearch.sql.ast.tree.Transpose node, CalcitePlanContext context) {

visitChildren(node, context);

int maxRows =
Optional.ofNullable(node.getMaxRows())
.filter(r -> r > 0)
.orElseThrow(() -> new IllegalArgumentException("maxRows must be positive"));

String columnName = node.getColumnName();
List<String> fieldNames = context.relBuilder.peek().getRowType().getFieldNames();

RelBuilder b = context.relBuilder;
RexBuilder rx = context.rexBuilder;
RelDataType varchar = rx.getTypeFactory().createSqlType(SqlTypeName.VARCHAR);

// Step 1: ROW_NUMBER
b.projectPlus(
b.aggregateCall(SqlStdOperatorTable.ROW_NUMBER)
.over()
.rowsTo(RexWindowBounds.CURRENT_ROW)
.as(PlanUtils.ROW_NUMBER_COLUMN_FOR_TRANSPOSE));

// Step 2: UNPIVOT
b.unpivot(
false,
ImmutableList.of("value"),
ImmutableList.of(columnName),
fieldNames.stream()
.map(
f ->
Map.entry(
ImmutableList.of(rx.makeLiteral(f)),
ImmutableList.of((RexNode) rx.makeCast(varchar, b.field(f), true))))
.collect(Collectors.toList()));

// Step 3: PIVOT
b.pivot(
b.groupKey(b.field(columnName)),
ImmutableList.of(b.max(b.field("value"))),
ImmutableList.of(b.field(PlanUtils.ROW_NUMBER_COLUMN_FOR_TRANSPOSE)),
IntStream.rangeClosed(1, maxRows)
.mapToObj(i -> Map.entry("row " + i, ImmutableList.of((RexNode) b.literal(i))))
.collect(Collectors.toList()));

// Step 4: RENAME
List<String> cleanNames = new ArrayList<>();
cleanNames.add(columnName);
for (int i = 1; i <= maxRows; i++) {
cleanNames.add("row " + i);
}
b.rename(cleanNames);

return b.peek();
}

@Override
public RelNode visitBin(Bin node, CalcitePlanContext context) {
visitChildren(node, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public interface PlanUtils {
String ROW_NUMBER_COLUMN_FOR_SUBSEARCH = "_row_number_subsearch_";
String ROW_NUMBER_COLUMN_FOR_STREAMSTATS = "__stream_seq__";
String ROW_NUMBER_COLUMN_FOR_CHART = "_row_number_chart_";
String ROW_NUMBER_COLUMN_FOR_TRANSPOSE = "_row_number_transpose_";

static SpanUnit intervalUnitToSpanUnit(IntervalUnit unit) {
return switch (unit) {
Expand Down
1 change: 1 addition & 0 deletions docs/category.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"user/ppl/cmd/timechart.md",
"user/ppl/cmd/top.md",
"user/ppl/cmd/trendline.md",
"user/ppl/cmd/transpose.md",
"user/ppl/cmd/where.md",
"user/ppl/functions/aggregations.md",
"user/ppl/functions/collection.md",
Expand Down
92 changes: 92 additions & 0 deletions docs/user/ppl/cmd/transpose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# transpose

## Description

The `transpose` command outputs the requested number of rows as columns, effectively transposing each result row into a corresponding column of field values.

## Syntax

transpose [int] [column_name=<string>]

* number-of-rows: optional. The number of rows to transform into columns. Default value is 5. Maximum allowed is 10000.
* column_name: optional. The name of the first column to use when transposing rows. This column holds the field names.


## Example 1: Transpose results

This example shows transposing wihtout any parameters. It transforms 5 rows into columns as default is 5.

```ppl
source=accounts
| head 5
| fields account_number, firstname, lastname, balance
| transpose
```

Expected output:

```text
fetched rows / total rows = 4/4
+----------------+-------+--------+---------+-------+-------+
| column | row 1 | row 2 | row 3 | row 4 | row 5 |
|----------------+-------+--------+---------+-------+-------|
| account_number | 1 | 6 | 13 | 18 | null |
| firstname | Amber | Hattie | Nanette | Dale | null |
| balance | 39225 | 5686 | 32838 | 4180 | null |
| lastname | Duke | Bond | Bates | Adams | null |
+----------------+-------+--------+---------+-------+-------+
```

## Example 2: Tranpose results up to a provided number of rows.

This example shows transposing wihtout any parameters. It transforms 4 rows into columns as default is 5.

```ppl
source=accounts
| head 5
| fields account_number, firstname, lastname, balance
| transpose 4
```

Expected output:

```text
fetched rows / total rows = 4/4
+----------------+-------+--------+---------+-------+
| column | row 1 | row 2 | row 3 | row 4 |
|----------------+-------+--------+---------+-------|
| account_number | 1 | 6 | 13 | 18 |
| firstname | Amber | Hattie | Nanette | Dale |
| balance | 39225 | 5686 | 32838 | 4180 |
| lastname | Duke | Bond | Bates | Adams |
+----------------+-------+--------+---------+-------+
```

## Example 2: Tranpose results up to a provided number of rows and first column with specified column name.

This example shows transposing wihtout any parameters. It transforms 4 rows into columns as default is 5.

```ppl
source=accounts
| head 5
| fields account_number, firstname, lastname, balance
| transpose 4 column_name='column_names'
```

Expected output:

```text
fetched rows / total rows = 4/4
+----------------+-------+--------+---------+-------+
| column_names | row 1 | row 2 | row 3 | row 4 |
|----------------+-------+--------+---------+-------|
| account_number | 1 | 6 | 13 | 18 |
| firstname | Amber | Hattie | Nanette | Dale |
| balance | 39225 | 5686 | 32838 | 4180 |
| lastname | Duke | Bond | Bates | Adams |
+----------------+-------+--------+---------+-------+
```

## Limitations

The `transpose` command transforms up to a number of rows specified and if not enough rows found, it shows those transposed rows as null columns.
7 changes: 4 additions & 3 deletions docs/user/ppl/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ source=accounts
| [describe command](cmd/describe.md) | 2.1 | stable (since 2.1) | Query the metadata of an index. |
| [explain command](cmd/explain.md) | 3.1 | stable (since 3.1) | Explain the plan of query. |
| [show datasources command](cmd/showdatasources.md) | 2.4 | stable (since 2.4) | Query datasources configured in the PPL engine. |
| [addtotals command](cmd/addtotals.md) | 3.4 | stable (since 3.4) | Adds row and column values and appends a totals column and row. |
| [addcoltotals command](cmd/addcoltotals.md) | 3.4 | stable (since 3.4) | Adds column values and appends a totals row. |

| [addtotals command](cmd/addtotals.md) | 3.5 | stable (since 3.5) | Adds row and column values and appends a totals column and row. |
| [addcoltotals command](cmd/addcoltotals.md) | 3.5 | stable (since 3.5) | Adds column values and appends a totals row. |
| [transpose command](cmd/transpose.md) | 3.5 | stable (since 3.5) | Transpose rows to columns. |

- [Syntax](cmd/syntax.md) - PPL query structure and command syntax formatting
* **Functions**
- [Aggregation Functions](functions/aggregations.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
CalciteTextFunctionIT.class,
CalciteTopCommandIT.class,
CalciteTrendlineCommandIT.class,
CalciteTransposeCommandIT.class,
CalciteVisualizationFormatIT.class,
CalciteWhereCommandIT.class,
CalcitePPLTpchIT.class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,18 @@ public void testaddColTotalsExplain() throws IOException {
+ "| addcoltotals balance age label='GrandTotal'"));
}

@Test
public void testTransposeExplain() throws IOException {
enabledOnlyWhenPushdownIsEnabled();
String expected = loadExpectedPlan("explain_transpose.yaml");
assertYamlEqualsIgnoreId(
expected,
explainQueryYaml(
"source=opensearch-sql_test_index_account"
+ "| head 5 "
+ "| transpose 4 column_name='column_names'"));
}

public void testComplexDedup() throws IOException {
enabledOnlyWhenPushdownIsEnabled();
String expected = loadExpectedPlan("explain_dedup_complex1.yaml");
Expand Down
Loading
Loading