Skip to content

Commit e1a1bd8

Browse files
authored
PPL fillnull command enhancement (#4421)
* PPL fillnull command enhancement Signed-off-by: Kai Huang <[email protected]> # Conflicts: # integ-test/src/test/java/org/opensearch/sql/calcite/CalciteNoPushdownIT.java * add to searchableKeyWord Signed-off-by: Kai Huang <[email protected]> * fixes Signed-off-by: Kai Huang <[email protected]> * fix CI Signed-off-by: Kai Huang <[email protected]> * update error message handling Signed-off-by: Kai Huang <[email protected]> * formatting Signed-off-by: Kai Huang <[email protected]> * put file back Signed-off-by: Kai Huang <[email protected]> * removal Signed-off-by: Kai Huang <[email protected]> * update doc Signed-off-by: Kai Huang <[email protected]> * update doc Signed-off-by: Kai Huang <[email protected]> * add IT Signed-off-by: Kai Huang <[email protected]> --------- Signed-off-by: Kai Huang <[email protected]>
1 parent 416a327 commit e1a1bd8

File tree

16 files changed

+484
-21
lines changed

16 files changed

+484
-21
lines changed

core/src/main/java/org/opensearch/sql/ast/dsl/AstDSL.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,11 +596,25 @@ public static FillNull fillNull(UnresolvedPlan input, UnresolvedExpression repla
596596
return FillNull.ofSameValue(replacement, ImmutableList.of()).attach(input);
597597
}
598598

599+
public static FillNull fillNull(
600+
UnresolvedPlan input, UnresolvedExpression replacement, boolean useValueSyntax) {
601+
return FillNull.ofSameValue(replacement, ImmutableList.of(), useValueSyntax).attach(input);
602+
}
603+
599604
public static FillNull fillNull(
600605
UnresolvedPlan input, UnresolvedExpression replacement, Field... fields) {
601606
return FillNull.ofSameValue(replacement, ImmutableList.copyOf(fields)).attach(input);
602607
}
603608

609+
public static FillNull fillNull(
610+
UnresolvedPlan input,
611+
UnresolvedExpression replacement,
612+
boolean useValueSyntax,
613+
Field... fields) {
614+
return FillNull.ofSameValue(replacement, ImmutableList.copyOf(fields), useValueSyntax)
615+
.attach(input);
616+
}
617+
604618
public static FillNull fillNull(
605619
UnresolvedPlan input, List<Pair<Field, UnresolvedExpression>> fieldAndReplacements) {
606620
ImmutableList.Builder<Pair<Field, UnresolvedExpression>> replacementsBuilder =

core/src/main/java/org/opensearch/sql/ast/tree/FillNull.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@
2424
public class FillNull extends UnresolvedPlan {
2525

2626
public static FillNull ofVariousValue(List<Pair<Field, UnresolvedExpression>> replacements) {
27-
return new FillNull(replacements);
27+
return new FillNull(replacements, false);
2828
}
2929

3030
public static FillNull ofSameValue(UnresolvedExpression replacement, List<Field> fieldList) {
31+
return ofSameValue(replacement, fieldList, false);
32+
}
33+
34+
public static FillNull ofSameValue(
35+
UnresolvedExpression replacement, List<Field> fieldList, boolean useValueSyntax) {
3136
List<Pair<Field, UnresolvedExpression>> replacementPairs =
3237
fieldList.stream().map(f -> Pair.of(f, replacement)).toList();
33-
FillNull instance = new FillNull(replacementPairs);
38+
FillNull instance = new FillNull(replacementPairs, useValueSyntax);
3439
if (replacementPairs.isEmpty()) {
3540
// no field specified, the replacement value will be applied to all fields.
3641
instance.replacementForAll = Optional.of(replacement);
@@ -42,8 +47,14 @@ public static FillNull ofSameValue(UnresolvedExpression replacement, List<Field>
4247

4348
private final List<Pair<Field, UnresolvedExpression>> replacementPairs;
4449

45-
FillNull(List<Pair<Field, UnresolvedExpression>> replacementPairs) {
50+
// Track if value= syntax was used (added in 3.4). Only needed to distinguish from with...in
51+
// since both apply same value to all fields. using syntax is detected by checking if all
52+
// replacement values are the same.
53+
private final boolean useValueSyntax;
54+
55+
FillNull(List<Pair<Field, UnresolvedExpression>> replacementPairs, boolean useValueSyntax) {
4656
this.replacementPairs = replacementPairs;
57+
this.useValueSyntax = useValueSyntax;
4758
}
4859

4960
private UnresolvedPlan child;

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.apache.calcite.rel.logical.LogicalAggregate;
5050
import org.apache.calcite.rel.logical.LogicalValues;
5151
import org.apache.calcite.rel.type.RelDataType;
52+
import org.apache.calcite.rel.type.RelDataTypeFamily;
5253
import org.apache.calcite.rel.type.RelDataTypeField;
5354
import org.apache.calcite.rex.RexCall;
5455
import org.apache.calcite.rex.RexCorrelVariable;
@@ -1493,6 +1494,30 @@ public RelNode visitWindow(Window node, CalcitePlanContext context) {
14931494
return context.relBuilder.peek();
14941495
}
14951496

1497+
/**
1498+
* Validates type compatibility between replacement value and field for fillnull operation. Throws
1499+
* SemanticCheckException if types are incompatible.
1500+
*/
1501+
private void validateFillNullTypeCompatibility(
1502+
RexNode replacement, RexNode fieldRef, String fieldName) {
1503+
RelDataTypeFamily replacementFamily = replacement.getType().getFamily();
1504+
RelDataTypeFamily fieldFamily = fieldRef.getType().getFamily();
1505+
1506+
// Check if the replacement type is compatible with the field type
1507+
// Allow NULL type family as it's compatible with any type
1508+
if (fieldFamily != replacementFamily
1509+
&& fieldFamily != SqlTypeFamily.NULL
1510+
&& replacementFamily != SqlTypeFamily.NULL) {
1511+
throw new SemanticCheckException(
1512+
String.format(
1513+
"fillnull failed: replacement value type %s is not compatible with field '%s' "
1514+
+ "(type: %s). The replacement value type must match the field type.",
1515+
replacement.getType().getSqlTypeName(),
1516+
fieldName,
1517+
fieldRef.getType().getSqlTypeName()));
1518+
}
1519+
}
1520+
14961521
@Override
14971522
public RelNode visitFillNull(FillNull node, CalcitePlanContext context) {
14981523
visitChildren(node, context);
@@ -1501,6 +1526,19 @@ public RelNode visitFillNull(FillNull node, CalcitePlanContext context) {
15011526
.size()) {
15021527
throw new IllegalArgumentException("The field list cannot be duplicated in fillnull");
15031528
}
1529+
1530+
// Validate type compatibility when replacementForAll is present
1531+
if (node.getReplacementForAll().isPresent()) {
1532+
List<RelDataTypeField> fieldsList = context.relBuilder.peek().getRowType().getFieldList();
1533+
RexNode replacement = rexVisitor.analyze(node.getReplacementForAll().get(), context);
1534+
1535+
// Validate all fields are compatible with the replacement value
1536+
for (RelDataTypeField field : fieldsList) {
1537+
RexNode fieldRef = context.rexBuilder.makeInputRef(field.getType(), field.getIndex());
1538+
validateFillNullTypeCompatibility(replacement, fieldRef, field.getName());
1539+
}
1540+
}
1541+
15041542
List<RexNode> projects = new ArrayList<>();
15051543
List<RelDataTypeField> fieldsList = context.relBuilder.peek().getRowType().getFieldList();
15061544
for (RelDataTypeField field : fieldsList) {
@@ -1509,6 +1547,8 @@ public RelNode visitFillNull(FillNull node, CalcitePlanContext context) {
15091547
for (Pair<Field, UnresolvedExpression> pair : node.getReplacementPairs()) {
15101548
if (field.getName().equalsIgnoreCase(pair.getLeft().getField().toString())) {
15111549
RexNode replacement = rexVisitor.analyze(pair.getRight(), context);
1550+
// Validate type compatibility before COALESCE
1551+
validateFillNullTypeCompatibility(replacement, fieldRef, field.getName());
15121552
RexNode coalesce = context.rexBuilder.coalesce(fieldRef, replacement);
15131553
RexNode coalesceWithAlias = context.relBuilder.alias(coalesce, field.getName());
15141554
projects.add(coalesceWithAlias);

docs/user/ppl/cmd/fillnull.rst

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,33 @@ Using ``fillnull`` command to fill null with provided value in one or more field
1616

1717
Syntax
1818
============
19+
1920
fillnull with <replacement> [in <field-list>]
2021

2122
fillnull using <field> = <replacement> [, <field> = <replacement>]
2223

23-
* replacement: mandatory. The value used to replace `null`s.
24-
* field-list: optional. The comma-delimited field list. The `null` values in the field will be replaced with the values from the replacement. From 3.1.0, when ``plugins.calcite.enabled`` is true, if no field specified, the replacement is applied to all fields.
24+
fillnull value=<replacement> [<field-list>]
25+
26+
27+
Parameters
28+
============
29+
30+
* replacement: Mandatory. The value used to replace `null`s.
31+
32+
* field-list: Optional. Comma-delimited (when using ``with`` or ``using``) or space-delimited (when using ``value=``) list of fields. The `null` values in the field will be replaced with the values from the replacement. **Default:** If no field specified, the replacement is applied to all fields.
33+
34+
**Syntax Variations:**
35+
36+
* ``with <replacement> in <field-list>`` - Apply same value to specified fields
37+
* ``using <field>=<replacement>, ...`` - Apply different values to different fields
38+
* ``value=<replacement> [<field-list>]`` - Alternative syntax with optional space-delimited field list
39+
40+
41+
Examples
42+
============
2543

26-
Example 1: replace null values with a specified value on one field
27-
==================================================================
44+
Example 1: Replace null values with a specified value on one field
45+
-------------------------------------------------------------------
2846

2947
PPL query::
3048

@@ -39,8 +57,8 @@ PPL query::
3957
| [email protected] | null |
4058
+-----------------------+----------+
4159

42-
Example 2: replace null values with a specified value on multiple fields
43-
========================================================================
60+
Example 2: Replace null values with a specified value on multiple fields
61+
-------------------------------------------------------------------------
4462

4563
PPL query::
4664

@@ -55,10 +73,8 @@ PPL query::
5573
| [email protected] | <not found> |
5674
+-----------------------+-------------+
5775

58-
Example 3: replace null values with a specified value on all fields
59-
===================================================================
60-
61-
This example only works when Calcite enabled.
76+
Example 3: Replace null values with a specified value on all fields
77+
--------------------------------------------------------------------
6278

6379
PPL query::
6480

@@ -73,8 +89,8 @@ PPL query::
7389
| [email protected] | <not found> |
7490
+-----------------------+-------------+
7591

76-
Example 4: replace null values with multiple specified values on multiple fields
77-
================================================================================
92+
Example 4: Replace null values with multiple specified values on multiple fields
93+
---------------------------------------------------------------------------------
7894

7995
PPL query::
8096

@@ -90,7 +106,51 @@ PPL query::
90106
+-----------------------+---------------+
91107

92108

109+
Example 5: Replace null with specified value on specific fields (value= syntax)
110+
--------------------------------------------------------------------------------
111+
112+
PPL query::
113+
114+
os> source=accounts | fields email, employer | fillnull value="<not found>" email employer;
115+
fetched rows / total rows = 4/4
116+
+-----------------------+-------------+
117+
| email | employer |
118+
|-----------------------+-------------|
119+
| [email protected] | Pyrami |
120+
| [email protected] | Netagy |
121+
| <not found> | Quility |
122+
| [email protected] | <not found> |
123+
+-----------------------+-------------+
124+
125+
Example 6: Replace null with specified value on all fields (value= syntax)
126+
---------------------------------------------------------------------------
127+
128+
When no field list is specified, the replacement applies to all fields in the result.
129+
130+
PPL query::
131+
132+
os> source=accounts | fields email, employer | fillnull value='<not found>';
133+
fetched rows / total rows = 4/4
134+
+-----------------------+-------------+
135+
| email | employer |
136+
|-----------------------+-------------|
137+
| [email protected] | Pyrami |
138+
| [email protected] | Netagy |
139+
| <not found> | Quility |
140+
| [email protected] | <not found> |
141+
+-----------------------+-------------+
142+
93143
Limitations
94-
===========
144+
============
95145
* The ``fillnull`` command is not rewritten to OpenSearch DSL, it is only executed on the coordination node.
96-
* Before 3.1.0, at least one field is required.
146+
* When applying the same value to all fields without specifying field names, all fields must be the same type. For mixed types, use separate fillnull commands or explicitly specify fields.
147+
* The replacement value type must match ALL field types in the field list. When applying the same value to multiple fields, all fields must be the same type (all strings or all numeric).
148+
149+
**Example:**
150+
151+
.. code-block:: sql
152+
153+
# This FAILS - same value for mixed-type fields
154+
source=accounts | fillnull value=0 firstname, age
155+
# ERROR: fillnull failed: replacement value type INTEGER is not compatible with field 'firstname' (type: VARCHAR). The replacement value type must match the field type.
156+

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteExplainIT.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,4 +936,14 @@ public void testExplainPushDownScriptsContainingUDT() throws IOException {
936936
+ " span(t, 1d)",
937937
TEST_INDEX_BANK)));
938938
}
939+
940+
@Test
941+
public void testFillNullValueSyntaxExplain() throws IOException {
942+
String expected = loadExpectedPlan("explain_fillnull_value_syntax.yaml");
943+
assertYamlEqualsJsonIgnoreId(
944+
expected,
945+
explainQueryToString(
946+
String.format(
947+
"source=%s | fields age, balance | fillnull value=0", TEST_INDEX_ACCOUNT)));
948+
}
939949
}

0 commit comments

Comments
 (0)