Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d02ff60
fielfformat changes
asifabashar Jan 14, 2026
3c62d86
added field format with concatenation of string , added more examples
asifabashar Jan 24, 2026
553ffa7
added field format with concatenation of string , added more examples
asifabashar Jan 24, 2026
36085f5
ci failure fix test
asifabashar Jan 26, 2026
c08beda
ci failure fix test
asifabashar Jan 26, 2026
faaf0f9
ci failure fix test
asifabashar Jan 26, 2026
b6e8965
ci failure fix test
asifabashar Jan 27, 2026
330cfc0
added missing test files
asifabashar Jan 27, 2026
d853f36
added missing test files
asifabashar Jan 27, 2026
c74151d
doc fix
asifabashar Jan 27, 2026
6c93ebe
doc fix
asifabashar Jan 27, 2026
239df5a
doc fix
asifabashar Jan 27, 2026
be0d82b
added test
asifabashar Jan 27, 2026
090bea7
[Feature] implement transpose command as in the roadmap #4786 (#5011)
asifabashar Jan 27, 2026
fcd0f87
added test
asifabashar Jan 27, 2026
1ff53f6
[Feature] implement transpose command as in the roadmap #4786 (#5011)
asifabashar Jan 27, 2026
4fc4113
fielfformat changes
asifabashar Jan 14, 2026
43ec5c9
added field format with concatenation of string , added more examples
asifabashar Jan 24, 2026
90c88fc
added test
asifabashar Jan 27, 2026
8725e4d
[Feature] implement transpose command as in the roadmap #4786 (#5011)
asifabashar Jan 27, 2026
3e3908f
added test
asifabashar Jan 27, 2026
99673af
merge from main
asifabashar Jan 27, 2026
8ead775
merge conflict issues
asifabashar Jan 27, 2026
538efac
merge conflict issues
asifabashar Jan 27, 2026
8ec8887
doc fix
asifabashar Jan 27, 2026
c57b38f
test fix
asifabashar Jan 27, 2026
2f6cd1a
test fix
asifabashar Jan 27, 2026
7b05103
coderabbit recommendations
asifabashar Jan 28, 2026
ffd33bd
resolve conflicts
asifabashar Jan 28, 2026
494df1d
resolve conflicts
asifabashar Jan 28, 2026
7543224
resolve conflicts
asifabashar Jan 28, 2026
1a5d916
resolve conflicts
asifabashar Jan 28, 2026
54b3a06
resolve conflicts
asifabashar Jan 28, 2026
f5703a4
removed ambigous sentence
asifabashar Jan 30, 2026
72bef4b
added missing test files
asifabashar Jan 27, 2026
b9b2908
spotlessApply
asifabashar Feb 3, 2026
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 @@ -526,6 +526,12 @@ public LogicalPlan visitEval(Eval node, AnalysisContext context) {
return new LogicalEval(child, expressionsBuilder.build());
}

/** Build {@link LogicalEval}. */
@Override
public LogicalPlan visitFieldFormat(Eval node, AnalysisContext context) {
throw getOnlyForCalciteException("fieldformat");
}

@Override
public LogicalPlan visitAddTotals(AddTotals node, AnalysisContext context) {
throw getOnlyForCalciteException("addtotals");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ public T visitEval(Eval node, C context) {
return visitChildren(node, context);
}

public T visitFieldFormat(Eval node, C context) {
return visitChildren(node, context);
}

public T visitParse(Parse node, C context) {
return visitChildren(node, context);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,12 @@ public Node visitAddColTotals(AddColTotals node, FieldResolutionContext context)
return node;
}

@Override
public Node visitFieldFormat(Eval node, FieldResolutionContext context) {
visitChildren(node, context);
return node;
}

@Override
public Node visitExpand(Expand node, FieldResolutionContext context) {
Set<String> expandFields = extractFieldsFromExpression(node.getField());
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/expression/Let.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
public class Let extends UnresolvedExpression {
private final Field var;
private final UnresolvedExpression expression;
private final Literal concatPrefix;
private final Literal concatSuffix;

public Let(Field var, UnresolvedExpression expression) {
String varName = var.getField().toString();
Expand All @@ -29,6 +31,21 @@ public Let(Field var, UnresolvedExpression expression) {
}
this.var = var;
this.expression = expression;
this.concatPrefix = null;
this.concatSuffix = null;
}

public Let(
Field var, UnresolvedExpression expression, Literal concatPrefix, Literal concatSuffix) {
String varName = var.getField().toString();
if (OpenSearchConstants.METADATAFIELD_TYPE_MAP.containsKey(varName)) {
throw new IllegalArgumentException(
String.format("Cannot use metadata field [%s] as the eval field.", varName));
}
this.var = var;
this.expression = expression;
this.concatPrefix = concatPrefix;
this.concatSuffix = concatSuffix;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,28 @@ public RexNode visitLambdaFunction(LambdaFunction node, CalcitePlanContext conte
@Override
public RexNode visitLet(Let node, CalcitePlanContext context) {
RexNode expr = analyze(node.getExpression(), context);
if (node.getConcatPrefix() != null) {

expr =
context.rexBuilder.makeCall(
SqlStdOperatorTable.CONCAT,
context.rexBuilder.makeLiteral(
node.getConcatPrefix().getValue(),
context.rexBuilder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR),
true),
expr);
}
if (node.getConcatSuffix() != null) {

expr =
context.rexBuilder.makeCall(
SqlStdOperatorTable.CONCAT,
expr,
context.rexBuilder.makeLiteral(
node.getConcatSuffix().getValue(),
context.rexBuilder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR),
true));
}
return context.relBuilder.alias(expr, node.getVar().getField().toString());
}

Expand Down
1 change: 1 addition & 0 deletions docs/category.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"user/ppl/cmd/describe.md",
"user/ppl/cmd/eventstats.md",
"user/ppl/cmd/eval.md",
"user/ppl/cmd/fieldformat.md",
"user/ppl/cmd/fields.md",
"user/ppl/cmd/fillnull.md",
"user/ppl/cmd/grok.md",
Expand Down
128 changes: 128 additions & 0 deletions docs/user/ppl/cmd/fieldformat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@

# fieldformat

The `fieldformat` command sets the value to a field with the specified expression and appends the field with evaluated result to the search results. The command is an alias of eval command.
Additionally, it also provides string concatenation dot operator followed by and/or follows a string that will be concatenated to the expression.


## Syntax

The `fieldformat` command has the following syntax:

```syntax
fieldformat <field>=[(prefix).]<expression>[.(suffix)] ["," <field>=[(prefix).]<expression>[.(suffix)] ]...

```

## Parameters

The `fieldformat` command supports the following parameters.

| Parameter| Required/Optional | Description |
|----------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| `<field>` | Required | The name of the field to create or update. If the field does not exist, a new field is added. If it already exists, its value is overwritten. |
| `<expression>` | Required | The expression to evaluate. The expression can have a prefix and/or suffix string part that will be concatenated to the expression. |
| `prefix` | Optional | A string before the expression followed by dot operator which will be concatenated as prefix to the evaluated expression value. |
| `suffix` | Optional | A string that follows the expression and dot operator which will be concatenated as suffix to the evaluated expression value. |


## Example 1: Create a new field

The following query creates a new `doubleAge` field for each document:

```ppl
source=accounts
| fieldformat doubleAge = age * 2
| fields age, doubleAge
```

The query returns the following results:

```text
fetched rows / total rows = 4/4
+-----+-----------+
| age | doubleAge |
|-----+-----------|
| 32 | 64 |
| 36 | 72 |
| 28 | 56 |
| 33 | 66 |
+-----+-----------+
```


## Example 2: Override an existing field

The following query overrides the `age` field by adding `1` to its value:

```ppl
source=accounts
| fieldformat age = age + 1
| fields age
```

The query returns the following results:

```text
fetched rows / total rows = 4/4
+-----+
| age |
|-----|
| 33 |
| 37 |
| 29 |
| 34 |
+-----+
```




## Example 3: String concatenation with prefix

The following query uses the `.` (dot) operator for string concatenation. You can concatenate string literals and field values as follows:

```ppl
source=accounts
| fieldformat greeting = 'Hello '.tostring( firstname)
| fields firstname, greeting
```

The query returns the following results:

```text
fetched rows / total rows = 4/4
+-----------+---------------+
| firstname | greeting |
|-----------+---------------|
| Amber | Hello Amber |
| Hattie | Hello Hattie |
| Nanette | Hello Nanette |
| Dale | Hello Dale |
+-----------+---------------+
```


## Example 4: String concatenation with dot operator, prefix and suffix

The following query performs prefix and suffix string concatenation operations using dot operator:

```ppl
source=accounts | fieldformat age_info = 'Age: '.CAST(age AS STRING).' years.' | fields firstname, age, age_info
```

The query returns the following results:

```text
fetched rows / total rows = 4/4
+-----------+-----+----------------+
| firstname | age | age_info |
|-----------+-----+----------------|
| Amber | 32 | Age: 32 years. |
| Hattie | 36 | Age: 36 years. |
| Nanette | 28 | Age: 28 years. |
| Dale | 33 | Age: 33 years. |
+-----------+-----+----------------+
```


Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
CalciteDedupCommandIT.class,
CalciteDescribeCommandIT.class,
CalciteExpandCommandIT.class,
CalciteFieldFormatCommandIT.class,
CalciteFieldsCommandIT.class,
CalciteFillNullCommandIT.class,
CalciteFlattenCommandIT.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import java.io.IOException;
import java.util.Locale;
import org.apache.commons.text.StringEscapeUtils;
import org.junit.Ignore;
import org.junit.Test;
import org.opensearch.sql.ast.statement.ExplainMode;
Expand Down Expand Up @@ -2497,4 +2498,19 @@ public void testExplainMvCombine() throws IOException {
String expected = loadExpectedPlan("explain_mvcombine.yaml");
assertYamlEqualsIgnoreId(expected, actual);
}

@Test
public void testFieldFormatExplain() throws Exception {

enabledOnlyWhenPushdownIsEnabled();
String expected = loadExpectedPlan("explain_field_format.yaml");
assertYamlEqualsIgnoreId(
expected,
explainQueryYaml(
StringEscapeUtils.escapeJson(
StringUtils.format(
"source=%s | head 5| fieldformat formatted_balance ="
+ " \"$\".tostring(balance,\"commas\") ",
TEST_INDEX_ACCOUNT))));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite.remote;

import static org.opensearch.sql.util.MatcherUtils.*;

import java.io.IOException;
import org.apache.commons.text.StringEscapeUtils;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.opensearch.client.Request;
import org.opensearch.sql.ppl.PPLIntegTestCase;

public class CalciteFieldFormatCommandIT extends PPLIntegTestCase {

@Override
public void init() throws Exception {
super.init();
enableCalcite();

loadIndex(Index.BANK);

// Create test data for string concatenation
Request request1 = new Request("PUT", "/test_eval/_doc/1?refresh=true");
request1.setJsonEntity("{\"name\": \"Alice\", \"age\": 25, \"title\": \"Engineer\"}");
client().performRequest(request1);

Request request2 = new Request("PUT", "/test_eval/_doc/2?refresh=true");
request2.setJsonEntity("{\"name\": \"Bob\", \"age\": 30, \"title\": \"Manager\"}");
client().performRequest(request2);

Request request3 = new Request("PUT", "/test_eval/_doc/3?refresh=true");
request3.setJsonEntity("{\"name\": \"Charlie\", \"age\": null, \"title\": \"Analyst\"}");
client().performRequest(request3);
}

@Test
public void testFieldFormatStringConcatenation() throws IOException {
JSONObject result =
executeQuery(
StringEscapeUtils.escapeJson(
"source=test_eval | fieldformat greeting = 'Hello ' + name"));
verifySchema(
result,
schema("name", "string"),
schema("title", "string"),
schema("age", "bigint"),
schema("greeting", "string"));
verifyDataRows(
result,
rows("Alice", "Engineer", 25, "Hello Alice"),
rows("Bob", "Manager", 30, "Hello Bob"),
rows("Charlie", "Analyst", null, "Hello Charlie"));
}

@Test
public void testFieldFormatStringConcatenationWithNullFieldToString() throws IOException {
JSONObject result =
executeQuery(
StringEscapeUtils.escapeJson(
"source=test_eval | fieldformat age_desc = \"Age: \".tostring(age,\"commas\") |"
+ " fields name, age, age_desc"));
verifySchema(
result, schema("name", "string"), schema("age", "bigint"), schema("age_desc", "string"));
verifyDataRows(
result,
rows("Alice", 25, "Age: 25"),
rows("Bob", 30, "Age: 30"),
rows("Charlie", null, null));
}

@Test
public void testFieldFormatStringConcatenationWithNullField() throws IOException {
JSONObject result =
executeQuery(
StringEscapeUtils.escapeJson(
"source=test_eval | fieldformat age_desc = \"Age: \".CAST(age AS STRING) | fields"
+ " name, age, age_desc"));
verifySchema(
result, schema("name", "string"), schema("age", "bigint"), schema("age_desc", "string"));
verifyDataRows(
result,
rows("Alice", 25, "Age: 25"),
rows("Bob", 30, "Age: 30"),
rows("Charlie", null, null));
}

@Test
public void testFieldFormatStringConcatWithSuffix() throws IOException {
JSONObject result =
executeQuery(
StringEscapeUtils.escapeJson(
"source=test_eval | fieldformat age_desc = CAST(age AS STRING).\" years\" | fields"
+ " name, age, age_desc"));
verifySchema(
result, schema("name", "string"), schema("age", "bigint"), schema("age_desc", "string"));
verifyDataRows(
result,
rows("Alice", 25, "25 years"),
rows("Bob", 30, "30 years"),
rows("Charlie", null, null));
}

@Test
public void testFieldFormatStringConcatWithPrefixSuffix() throws IOException {
JSONObject result =
executeQuery(
StringEscapeUtils.escapeJson(
"source=test_eval | fieldformat age_desc = \"Age: \".CAST(age AS STRING).\" years\""
+ " | fields name, age, age_desc"));
verifySchema(
result, schema("name", "string"), schema("age", "bigint"), schema("age_desc", "string"));
verifyDataRows(
result,
rows("Alice", 25, "Age: 25 years"),
rows("Bob", 30, "Age: 30 years"),
rows("Charlie", null, null));
}
}
Loading
Loading