From 5eed3f260deb951bb861452596827aefca49d7e4 Mon Sep 17 00:00:00 2001 From: Luca Cavenaghi Date: Fri, 27 Feb 2026 01:08:51 +0100 Subject: [PATCH] Add isnotempty PPL function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `isnotempty(field)` function to PPL that returns true when a field is not null and not an empty string — the logical negation of `isempty(field)`. Implementation: NOT(IS_NULL(arg) OR IS_EMPTY(arg)) - ANTLR grammar: added ISNOTEMPTY token and parser rules in all three grammar modules (ppl, language-grammar, async-query-core) - BuiltinFunctionName: added IS_NOT_EMPTY enum constant - PPLFuncImpTable: registered Calcite implementation - Integration test: testIsNotEmpty in CalcitePPLConditionBuiltinFunctionIT - Documentation: added ISNOTEMPTY section to condition functions docs Resolves opensearch-project#5182 Signed-off-by: Luca Cavenaghi --- .../src/main/antlr/OpenSearchPPLLexer.g4 | 1 + .../src/main/antlr/OpenSearchPPLParser.g4 | 6 ++++ .../function/BuiltinFunctionName.java | 1 + .../expression/function/PPLFuncImpTable.java | 12 +++++++ docs/user/ppl/functions/condition.md | 36 +++++++++++++++++-- docs/user/ppl/functions/index.md | 1 + .../CalcitePPLConditionBuiltinFunctionIT.java | 22 ++++++++++++ .../src/main/antlr4/OpenSearchPPLLexer.g4 | 1 + .../src/main/antlr4/OpenSearchPPLParser.g4 | 2 ++ ppl/src/main/antlr/OpenSearchPPLLexer.g4 | 1 + ppl/src/main/antlr/OpenSearchPPLParser.g4 | 1 + 11 files changed, 82 insertions(+), 2 deletions(-) diff --git a/async-query-core/src/main/antlr/OpenSearchPPLLexer.g4 b/async-query-core/src/main/antlr/OpenSearchPPLLexer.g4 index cb323f7942c..d66e492ddcb 100644 --- a/async-query-core/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/async-query-core/src/main/antlr/OpenSearchPPLLexer.g4 @@ -373,6 +373,7 @@ REPLACE: 'REPLACE'; REVERSE: 'REVERSE'; CAST: 'CAST'; ISEMPTY: 'ISEMPTY'; +ISNOTEMPTY: 'ISNOTEMPTY'; ISBLANK: 'ISBLANK'; // JSON TEXT FUNCTIONS diff --git a/async-query-core/src/main/antlr/OpenSearchPPLParser.g4 b/async-query-core/src/main/antlr/OpenSearchPPLParser.g4 index 0e1cf0db946..a473434cde4 100644 --- a/async-query-core/src/main/antlr/OpenSearchPPLParser.g4 +++ b/async-query-core/src/main/antlr/OpenSearchPPLParser.g4 @@ -468,6 +468,7 @@ positionFunction booleanExpression : booleanFunctionCall # booleanFunctionCallExpr | isEmptyExpression # isEmptyExpr + | isNotEmptyExpression # isNotEmptyExpr | valueExpressionList NOT? IN LT_SQR_PRTHS subSearch RT_SQR_PRTHS # inSubqueryExpr | EXISTS LT_SQR_PRTHS subSearch RT_SQR_PRTHS # existsSubqueryExpr | cidrMatchFunctionCall # cidrFunctionCallExpr @@ -477,6 +478,10 @@ booleanExpression : (ISEMPTY | ISBLANK) LT_PRTHS functionArg RT_PRTHS ; + isNotEmptyExpression + : ISNOTEMPTY LT_PRTHS functionArg RT_PRTHS + ; + caseFunction : CASE LT_PRTHS logicalExpression COMMA valueExpression (COMMA logicalExpression COMMA valueExpression)* (ELSE valueExpression)? RT_PRTHS ; @@ -860,6 +865,7 @@ textFunctionName | REPLACE | REVERSE | ISEMPTY + | ISNOTEMPTY | ISBLANK ; diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index 3171a09d39a..02943176b96 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -289,6 +289,7 @@ public enum BuiltinFunctionName { IS_PRESENT(FunctionName.of("ispresent")), IS_EMPTY(FunctionName.of("isempty")), + IS_NOT_EMPTY(FunctionName.of("isnotempty")), IS_BLANK(FunctionName.of("isblank")), ROW_NUMBER(FunctionName.of("row_number")), diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java index 89a8f59397b..986051c1436 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java @@ -91,6 +91,7 @@ import static org.opensearch.sql.expression.function.BuiltinFunctionName.INTERNAL_TRANSLATE3; import static org.opensearch.sql.expression.function.BuiltinFunctionName.IS_BLANK; import static org.opensearch.sql.expression.function.BuiltinFunctionName.IS_EMPTY; +import static org.opensearch.sql.expression.function.BuiltinFunctionName.IS_NOT_EMPTY; import static org.opensearch.sql.expression.function.BuiltinFunctionName.IS_NOT_NULL; import static org.opensearch.sql.expression.function.BuiltinFunctionName.IS_NULL; import static org.opensearch.sql.expression.function.BuiltinFunctionName.IS_PRESENT; @@ -1252,6 +1253,17 @@ void populate() { builder.makeCall(SqlStdOperatorTable.IS_NULL, arg), builder.makeCall(SqlStdOperatorTable.IS_EMPTY, arg)), PPLTypeChecker.family(SqlTypeFamily.ANY)); + register( + IS_NOT_EMPTY, + (FunctionImp1) + (builder, arg) -> + builder.makeCall( + SqlStdOperatorTable.NOT, + builder.makeCall( + SqlStdOperatorTable.OR, + builder.makeCall(SqlStdOperatorTable.IS_NULL, arg), + builder.makeCall(SqlStdOperatorTable.IS_EMPTY, arg))), + PPLTypeChecker.family(SqlTypeFamily.ANY)); register( IS_BLANK, (FunctionImp1) diff --git a/docs/user/ppl/functions/condition.md b/docs/user/ppl/functions/condition.md index 512b5edbbe6..4e8a5d5dafa 100644 --- a/docs/user/ppl/functions/condition.md +++ b/docs/user/ppl/functions/condition.md @@ -642,8 +642,40 @@ fetched rows / total rows = 4/4 | False | | True | null | +---------------+---------+-------------------+----------+ ``` - -## EARLIEST + +## ISNOTEMPTY + +### Description + +Usage: `isnotempty(field)` returns true if the field is not null and is not an empty string. + +**Argument type:** All supported data types. +**Return type:** `BOOLEAN` + +### Example + +```ppl +source=accounts +| eval temp = ifnull(employer, ' ') +| eval `isnotempty(employer)` = isnotempty(employer), `isnotempty(temp)` = isnotempty(temp) +| fields `isnotempty(temp)`, temp, `isnotempty(employer)`, employer +``` + +Expected output: + +```text +fetched rows / total rows = 4/4 ++------------------+---------+----------------------+----------+ +| isnotempty(temp) | temp | isnotempty(employer) | employer | +|------------------+---------+----------------------+----------| +| True | Pyrami | True | Pyrami | +| True | Netagy | True | Netagy | +| True | Quility | True | Quility | +| True | | False | null | ++------------------+---------+----------------------+----------+ +``` + +## EARLIEST ### Description diff --git a/docs/user/ppl/functions/index.md b/docs/user/ppl/functions/index.md index 146288e19dc..fb2f54e4b00 100644 --- a/docs/user/ppl/functions/index.md +++ b/docs/user/ppl/functions/index.md @@ -54,6 +54,7 @@ PPL supports a wide range of built-in functions for data processing and analysis - [ISPRESENT](condition.md/#ispresent) - [ISBLANK](condition.md/#isblank) - [ISEMPTY](condition.md/#isempty) + - [ISNOTEMPTY](condition.md/#isnotempty) - [EARLIEST](condition.md/#earliest) - [LATEST](condition.md/#latest) - [REGEXP_MATCH](condition.md/#regexp_match) diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLConditionBuiltinFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLConditionBuiltinFunctionIT.java index ad132f3eb7e..16d6ac866e0 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLConditionBuiltinFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLConditionBuiltinFunctionIT.java @@ -282,6 +282,28 @@ public void testIsEmpty() throws IOException { verifyDataRows(actual, rows(null, 10), rows("", 57)); } + @Test + public void testIsNotEmpty() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | where isnotempty(name) | fields name, age", + TEST_INDEX_STATE_COUNTRY_WITH_NULL)); + + verifySchema(actual, schema("name", "string"), schema("age", "int")); + + // isnotempty = NOT isempty. isempty returns (null,10) and ("",57). + // So isnotempty returns everything else: Jake, Hello, John, Jane, Kevin, whitespace. + verifyDataRows( + actual, + rows("Jake", 70), + rows("Hello", 30), + rows("John", 25), + rows("Jane", 20), + rows("Kevin", null), + rows(" ", 27)); + } + @Test public void testIsBlank() throws IOException { JSONObject actual = diff --git a/language-grammar/src/main/antlr4/OpenSearchPPLLexer.g4 b/language-grammar/src/main/antlr4/OpenSearchPPLLexer.g4 index b7dc4b7286d..3aa4cdf7a33 100644 --- a/language-grammar/src/main/antlr4/OpenSearchPPLLexer.g4 +++ b/language-grammar/src/main/antlr4/OpenSearchPPLLexer.g4 @@ -419,6 +419,7 @@ BETWEEN: 'BETWEEN'; CIDRMATCH: 'CIDRMATCH'; ISPRESENT: 'ISPRESENT'; ISEMPTY: 'ISEMPTY'; +ISNOTEMPTY: 'ISNOTEMPTY'; ISBLANK: 'ISBLANK'; // FLOWCONTROL FUNCTIONS diff --git a/language-grammar/src/main/antlr4/OpenSearchPPLParser.g4 b/language-grammar/src/main/antlr4/OpenSearchPPLParser.g4 index cae57b53181..65f1233b9fa 100644 --- a/language-grammar/src/main/antlr4/OpenSearchPPLParser.g4 +++ b/language-grammar/src/main/antlr4/OpenSearchPPLParser.g4 @@ -851,6 +851,7 @@ conditionFunctionBase | EARLIEST | LATEST | ISEMPTY + | ISNOTEMPTY | ISBLANK ; @@ -877,6 +878,7 @@ textFunctionName | REPLACE | REVERSE | ISEMPTY + | ISNOTEMPTY | ISBLANK ; diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 index fde9845e081..201210095ff 100644 --- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 @@ -463,6 +463,7 @@ CIDRMATCH: 'CIDRMATCH'; BETWEEN: 'BETWEEN'; ISPRESENT: 'ISPRESENT'; ISEMPTY: 'ISEMPTY'; +ISNOTEMPTY: 'ISNOTEMPTY'; ISBLANK: 'ISBLANK'; // COLLECTION FUNCTIONS diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4 index 7caf2be8070..8b4cd9a7e35 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -1378,6 +1378,7 @@ conditionFunctionName | JSON_VALID | ISPRESENT | ISEMPTY + | ISNOTEMPTY | ISBLANK | EARLIEST | LATEST