Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ad84a10
fielfformat changes
asifabashar Jan 14, 2026
e956256
added field format with concatenation of string , added more examples
asifabashar Jan 24, 2026
fee4033
added field format with concatenation of string , added more examples
asifabashar Jan 24, 2026
3aa3ea2
ci failure fix test
asifabashar Jan 26, 2026
d7edefb
ci failure fix test
asifabashar Jan 26, 2026
3991f4f
ci failure fix test
asifabashar Jan 26, 2026
95235de
ci failure fix test
asifabashar Jan 27, 2026
018050d
added missing test files
asifabashar Jan 27, 2026
b9081d6
added missing test files
asifabashar Jan 27, 2026
0b4981a
doc fix
asifabashar Jan 27, 2026
0a9808f
doc fix
asifabashar Jan 27, 2026
f33269b
doc fix
asifabashar Jan 27, 2026
caabe38
added test
asifabashar Jan 27, 2026
3fdf753
[Feature] implement transpose command as in the roadmap #4786 (#5011)
asifabashar Jan 27, 2026
d49d43a
added test
asifabashar Jan 27, 2026
24b0b2f
[Feature] implement transpose command as in the roadmap #4786 (#5011)
asifabashar Jan 27, 2026
186b33a
fielfformat changes
asifabashar Jan 14, 2026
9c3c12a
added field format with concatenation of string , added more examples
asifabashar Jan 24, 2026
8089974
added test
asifabashar Jan 27, 2026
a275205
[Feature] implement transpose command as in the roadmap #4786 (#5011)
asifabashar Jan 27, 2026
517a57c
added test
asifabashar Jan 27, 2026
9402c8e
merge from main
asifabashar Jan 27, 2026
5a2d420
merge conflict issues
asifabashar Jan 27, 2026
87adc88
doc fix
asifabashar Jan 27, 2026
e2d7cd8
test fix
asifabashar Jan 27, 2026
e64681f
test fix
asifabashar Jan 27, 2026
e1660e6
coderabbit recommendations
asifabashar Jan 28, 2026
dda54be
resolve conflicts
asifabashar Jan 28, 2026
de6ad4f
resolve conflicts
asifabashar Jan 28, 2026
520dc9d
resolve conflicts
asifabashar Jan 28, 2026
4d2eef3
resolve conflicts
asifabashar Jan 28, 2026
d33dd3a
removed ambigous sentence
asifabashar Jan 30, 2026
8a56b47
added missing test files
asifabashar Jan 27, 2026
c9a322d
spotlessApply
asifabashar Feb 3, 2026
520dbc4
merge conflict fix
asifabashar Feb 11, 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 @@ -398,6 +398,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 @@ -26,6 +26,7 @@

import java.io.IOException;
import java.util.Locale;
import org.apache.commons.text.StringEscapeUtils;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
Expand Down Expand Up @@ -2516,6 +2517,21 @@ public void testAggFilterOnNestedFields() throws IOException {
TEST_INDEX_CASCADED_NESTED)));
}

@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))));
}

@Test
public void testExplainMvCombine() throws IOException {
String query =
Expand Down
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