diff --git a/docs/reference/esql/functions/kibana/definition/not_in.json b/docs/reference/esql/functions/kibana/definition/not_in.json new file mode 100644 index 0000000000000..3fa25d793b503 --- /dev/null +++ b/docs/reference/esql/functions/kibana/definition/not_in.json @@ -0,0 +1,263 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", + "type" : "operator", + "operator" : "NOT IN", + "name" : "not_in", + "description" : "The `NOT IN` operator allows testing whether a field or expression does *not* equal any element in a list of literals, fields or expressions.", + "signatures" : [ + { + "params" : [ + { + "name" : "field", + "type" : "boolean", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "boolean", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "cartesian_point", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "cartesian_point", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "cartesian_shape", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "cartesian_shape", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "double", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "double", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geo_point", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "geo_point", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geo_shape", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "geo_shape", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "integer", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "integer", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "ip", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "ip", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "keyword", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "keyword", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "keyword", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "text", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "long", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "long", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "text", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "keyword", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "text", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "text", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "field", + "type" : "version", + "optional" : false, + "description" : "An expression." + }, + { + "name" : "inlist", + "type" : "version", + "optional" : false, + "description" : "A list of items." + } + ], + "variadic" : true, + "returnType" : "boolean" + } + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/esql/functions/kibana/definition/not_like.json b/docs/reference/esql/functions/kibana/definition/not_like.json new file mode 100644 index 0000000000000..bba70d14d7cb7 --- /dev/null +++ b/docs/reference/esql/functions/kibana/definition/not_like.json @@ -0,0 +1,47 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", + "type" : "operator", + "operator" : "NOT LIKE", + "name" : "not_like", + "description" : "Use `NOT LIKE` to filter data based on string patterns using wildcards. `NOT LIKE`\nusually acts on a field placed on the left-hand side of the operator, but it can\nalso act on a constant (literal) expression. The right-hand side of the operator\nrepresents the pattern.\n\nThe following wildcard characters are supported:\n\n* `*` matches zero or more characters.\n* `?` matches one character.", + "signatures" : [ + { + "params" : [ + { + "name" : "str", + "type" : "keyword", + "optional" : false, + "description" : "A literal expression." + }, + { + "name" : "pattern", + "type" : "keyword", + "optional" : false, + "description" : "Pattern." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "str", + "type" : "text", + "optional" : false, + "description" : "A literal expression." + }, + { + "name" : "pattern", + "type" : "keyword", + "optional" : false, + "description" : "Pattern." + } + ], + "variadic" : true, + "returnType" : "boolean" + } + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/esql/functions/kibana/definition/not_rlike.json b/docs/reference/esql/functions/kibana/definition/not_rlike.json new file mode 100644 index 0000000000000..09abd5ab567e8 --- /dev/null +++ b/docs/reference/esql/functions/kibana/definition/not_rlike.json @@ -0,0 +1,47 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", + "type" : "operator", + "operator" : "NOT RLIKE", + "name" : "not_rlike", + "description" : "Use `NOT RLIKE` to filter data based on string patterns using using\n<>. `NOT RLIKE` usually acts on a field placed on\nthe left-hand side of the operator, but it can also act on a constant (literal)\nexpression. The right-hand side of the operator represents the pattern.", + "signatures" : [ + { + "params" : [ + { + "name" : "str", + "type" : "keyword", + "optional" : false, + "description" : "A literal value." + }, + { + "name" : "pattern", + "type" : "keyword", + "optional" : false, + "description" : "A regular expression." + } + ], + "variadic" : true, + "returnType" : "boolean" + }, + { + "params" : [ + { + "name" : "str", + "type" : "text", + "optional" : false, + "description" : "A literal value." + }, + { + "name" : "pattern", + "type" : "keyword", + "optional" : false, + "description" : "A regular expression." + } + ], + "variadic" : true, + "returnType" : "boolean" + } + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/esql/functions/kibana/docs/not_in.md b/docs/reference/esql/functions/kibana/docs/not_in.md new file mode 100644 index 0000000000000..e9e5a7b384d1c --- /dev/null +++ b/docs/reference/esql/functions/kibana/docs/not_in.md @@ -0,0 +1,7 @@ + + +### NOT_IN +The `NOT IN` operator allows testing whether a field or expression does *not* equal any element in a list of literals, fields or expressions. + diff --git a/docs/reference/esql/functions/kibana/docs/not_like.md b/docs/reference/esql/functions/kibana/docs/not_like.md new file mode 100644 index 0000000000000..fd1cf7a68630f --- /dev/null +++ b/docs/reference/esql/functions/kibana/docs/not_like.md @@ -0,0 +1,15 @@ + + +### NOT_LIKE +Use `NOT LIKE` to filter data based on string patterns using wildcards. `NOT LIKE` +usually acts on a field placed on the left-hand side of the operator, but it can +also act on a constant (literal) expression. The right-hand side of the operator +represents the pattern. + +The following wildcard characters are supported: + +* `*` matches zero or more characters. +* `?` matches one character. + diff --git a/docs/reference/esql/functions/kibana/docs/not_rlike.md b/docs/reference/esql/functions/kibana/docs/not_rlike.md new file mode 100644 index 0000000000000..dac23438c1612 --- /dev/null +++ b/docs/reference/esql/functions/kibana/docs/not_rlike.md @@ -0,0 +1,10 @@ + + +### NOT_RLIKE +Use `NOT RLIKE` to filter data based on string patterns using using +<>. `NOT RLIKE` usually acts on a field placed on +the left-hand side of the operator, but it can also act on a constant (literal) +expression. The right-hand side of the operator represents the pattern. + diff --git a/docs/reference/esql/functions/types/not_in.asciidoc b/docs/reference/esql/functions/types/not_in.asciidoc new file mode 100644 index 0000000000000..6ed2c250ef0ac --- /dev/null +++ b/docs/reference/esql/functions/types/not_in.asciidoc @@ -0,0 +1,22 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Supported types* + +[%header.monospaced.styled,format=dsv,separator=|] +|=== +field | inlist | result +boolean | boolean | boolean +cartesian_point | cartesian_point | boolean +cartesian_shape | cartesian_shape | boolean +double | double | boolean +geo_point | geo_point | boolean +geo_shape | geo_shape | boolean +integer | integer | boolean +ip | ip | boolean +keyword | keyword | boolean +keyword | text | boolean +long | long | boolean +text | keyword | boolean +text | text | boolean +version | version | boolean +|=== diff --git a/docs/reference/esql/functions/types/not_like.asciidoc b/docs/reference/esql/functions/types/not_like.asciidoc new file mode 100644 index 0000000000000..fffa6dc0b8371 --- /dev/null +++ b/docs/reference/esql/functions/types/not_like.asciidoc @@ -0,0 +1,10 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Supported types* + +[%header.monospaced.styled,format=dsv,separator=|] +|=== +str | pattern | result +keyword | keyword | boolean +text | keyword | boolean +|=== diff --git a/docs/reference/esql/functions/types/not_rlike.asciidoc b/docs/reference/esql/functions/types/not_rlike.asciidoc new file mode 100644 index 0000000000000..fffa6dc0b8371 --- /dev/null +++ b/docs/reference/esql/functions/types/not_rlike.asciidoc @@ -0,0 +1,10 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Supported types* + +[%header.monospaced.styled,format=dsv,separator=|] +|=== +str | pattern | result +keyword | keyword | boolean +text | keyword | boolean +|=== diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java index 67dec69b51393..30ac9fc69ed9c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java @@ -823,12 +823,13 @@ public static void renderSignature() throws IOException { if (System.getProperty("generateDocs") == null) { return; } - String rendered = buildSignatureSvg(functionName()); + String name = functionName(); + String rendered = buildSignatureSvg(name); if (rendered == null) { LogManager.getLogger(getTestClass()).info("Skipping rendering signature because the function isn't registered"); } else { LogManager.getLogger(getTestClass()).info("Writing function signature"); - writeToTempDir("signature", rendered, "svg"); + writeToTempDir("signature", name, "svg", rendered); } } @@ -890,10 +891,13 @@ private static Map, DataType> signatures() { @AfterClass public static void renderDocs() throws IOException { + renderDocs(functionName()); + } + + protected static void renderDocs(String name) throws IOException { if (System.getProperty("generateDocs") == null) { return; } - String name = functionName(); if (binaryOperator(name) != null || unaryOperator(name) != null || searchOperator(name) != null || likeOrInOperator(name)) { renderDocsForOperators(name); return; @@ -922,12 +926,12 @@ public static void renderDocs() throws IOException { description.isAggregation() ); } - renderTypes(description.args()); - renderParametersList(description.argNames(), description.argDescriptions()); + renderTypes(name, description.args()); + renderParametersList(name, description.argNames(), description.argDescriptions()); FunctionInfo info = EsqlFunctionRegistry.functionInfo(definition); - renderDescription(description.description(), info.detailedDescription(), info.note()); - boolean hasExamples = renderExamples(info); - boolean hasAppendix = renderAppendix(info.appendix()); + renderDescription(name, description.description(), info.detailedDescription(), info.note()); + boolean hasExamples = renderExamples(name, info); + boolean hasAppendix = renderAppendix(name, info.appendix()); renderFullLayout(name, info.preview(), hasExamples, hasAppendix); renderKibanaInlineDocs(name, info); renderKibanaFunctionDefinition(name, info, description.args(), description.variadic()); @@ -944,7 +948,7 @@ public static void renderDocs() throws IOException { + "may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview " + "are not subject to the support SLA of official GA features.\"]\n"; - private static void renderTypes(List args) throws IOException { + private static void renderTypes(String name, List args) throws IOException { StringBuilder header = new StringBuilder(); List argNames = args.stream().map(EsqlFunctionRegistry.ArgSignature::name).toList(); for (String arg : argNames) { @@ -984,11 +988,11 @@ private static void renderTypes(List args) th [%header.monospaced.styled,format=dsv,separator=|] |=== """ + header + "\n" + table.stream().collect(Collectors.joining("\n")) + "\n|===\n"; - LogManager.getLogger(getTestClass()).info("Writing function types for [{}]:\n{}", functionName(), rendered); - writeToTempDir("types", rendered, "asciidoc"); + LogManager.getLogger(getTestClass()).info("Writing function types for [{}]:\n{}", name, rendered); + writeToTempDir("types", name, "asciidoc", rendered); } - private static void renderParametersList(List argNames, List argDescriptions) throws IOException { + private static void renderParametersList(String name, List argNames, List argDescriptions) throws IOException { StringBuilder builder = new StringBuilder(); builder.append(DOCS_WARNING); builder.append("*Parameters*\n"); @@ -996,11 +1000,11 @@ private static void renderParametersList(List argNames, List arg builder.append("\n`").append(argNames.get(a)).append("`::\n").append(argDescriptions.get(a)).append('\n'); } String rendered = builder.toString(); - LogManager.getLogger(getTestClass()).info("Writing parameters for [{}]:\n{}", functionName(), rendered); - writeToTempDir("parameters", rendered, "asciidoc"); + LogManager.getLogger(getTestClass()).info("Writing parameters for [{}]:\n{}", name, rendered); + writeToTempDir("parameters", name, "asciidoc", rendered); } - private static void renderDescription(String description, String detailedDescription, String note) throws IOException { + private static void renderDescription(String name, String description, String detailedDescription, String note) throws IOException { String rendered = DOCS_WARNING + """ *Description* @@ -1013,11 +1017,11 @@ private static void renderDescription(String description, String detailedDescrip if (Strings.isNullOrEmpty(note) == false) { rendered += "\nNOTE: " + note + "\n"; } - LogManager.getLogger(getTestClass()).info("Writing description for [{}]:\n{}", functionName(), rendered); - writeToTempDir("description", rendered, "asciidoc"); + LogManager.getLogger(getTestClass()).info("Writing description for [{}]:\n{}", name, rendered); + writeToTempDir("description", name, "asciidoc", rendered); } - private static boolean renderExamples(FunctionInfo info) throws IOException { + private static boolean renderExamples(String name, FunctionInfo info) throws IOException { if (info == null || info.examples().length == 0) { return false; } @@ -1051,20 +1055,20 @@ private static boolean renderExamples(FunctionInfo info) throws IOException { } builder.append('\n'); String rendered = builder.toString(); - LogManager.getLogger(getTestClass()).info("Writing examples for [{}]:\n{}", functionName(), rendered); - writeToTempDir("examples", rendered, "asciidoc"); + LogManager.getLogger(getTestClass()).info("Writing examples for [{}]:\n{}", name, rendered); + writeToTempDir("examples", name, "asciidoc", rendered); return true; } - private static boolean renderAppendix(String appendix) throws IOException { + private static boolean renderAppendix(String name, String appendix) throws IOException { if (appendix.isEmpty()) { return false; } String rendered = DOCS_WARNING + appendix + "\n"; - LogManager.getLogger(getTestClass()).info("Writing appendix for [{}]:\n{}", functionName(), rendered); - writeToTempDir("appendix", rendered, "asciidoc"); + LogManager.getLogger(getTestClass()).info("Writing appendix for [{}]:\n{}", name, rendered); + writeToTempDir("appendix", name, "asciidoc", rendered); return true; } @@ -1091,11 +1095,11 @@ private static void renderFullLayout(String name, boolean preview, boolean hasEx if (hasAppendix) { rendered += "include::../appendix/" + name + ".asciidoc[]\n"; } - LogManager.getLogger(getTestClass()).info("Writing layout for [{}]:\n{}", functionName(), rendered); - writeToTempDir("layout", rendered, "asciidoc"); + LogManager.getLogger(getTestClass()).info("Writing layout for [{}]:\n{}", name, rendered); + writeToTempDir("layout", name, "asciidoc", rendered); } - private static Constructor constructorWithFunctionInfo(Class clazz) { + protected static Constructor constructorWithFunctionInfo(Class clazz) { for (Constructor ctor : clazz.getConstructors()) { FunctionInfo functionInfo = ctor.getAnnotation(FunctionInfo.class); if (functionInfo != null) { @@ -1110,6 +1114,10 @@ private static void renderDocsForOperators(String name) throws IOException { assert ctor != null; FunctionInfo functionInfo = ctor.getAnnotation(FunctionInfo.class); assert functionInfo != null; + renderDocsForOperators(name, ctor, functionInfo); + } + + protected static void renderDocsForOperators(String name, Constructor ctor, FunctionInfo functionInfo) throws IOException { renderKibanaInlineDocs(name, functionInfo); var params = ctor.getParameters(); @@ -1127,7 +1135,7 @@ private static void renderDocsForOperators(String name) throws IOException { } } renderKibanaFunctionDefinition(name, functionInfo, args, likeOrInOperator(name)); - renderTypes(args); + renderTypes(name, args); } private static void renderKibanaInlineDocs(String name, FunctionInfo info) throws IOException { @@ -1151,8 +1159,8 @@ private static void renderKibanaInlineDocs(String name, FunctionInfo info) throw builder.append("Note: ").append(removeAsciidocLinks(info.note())).append("\n"); } String rendered = builder.toString(); - LogManager.getLogger(getTestClass()).info("Writing kibana inline docs for [{}]:\n{}", functionName(), rendered); - writeToTempDir("kibana/docs", rendered, "md"); + LogManager.getLogger(getTestClass()).info("Writing kibana inline docs for [{}]:\n{}", name, rendered); + writeToTempDir("kibana/docs", name, "md", rendered); } private static void renderKibanaFunctionDefinition( @@ -1244,8 +1252,8 @@ private static void renderKibanaFunctionDefinition( builder.field("snapshot_only", EsqlFunctionRegistry.isSnapshotOnly(name)); String rendered = Strings.toString(builder.endObject()); - LogManager.getLogger(getTestClass()).info("Writing kibana function definition for [{}]:\n{}", functionName(), rendered); - writeToTempDir("kibana/definition", rendered, "json"); + LogManager.getLogger(getTestClass()).info("Writing kibana function definition for [{}]:\n{}", name, rendered); + writeToTempDir("kibana/definition", name, "json", rendered); } private static String removeAsciidocLinks(String asciidoc) { @@ -1340,7 +1348,10 @@ private static String unaryOperator(String name) { * If this tests is for a like or rlike operator return true, otherwise return {@code null}. */ private static boolean likeOrInOperator(String name) { - return name.equalsIgnoreCase("rlike") || name.equalsIgnoreCase("like") || name.equalsIgnoreCase("in"); + return switch (name.toLowerCase(Locale.ENGLISH)) { + case "rlike", "like", "in", "not_rlike", "not_like", "not_in" -> true; + default -> false; + }; } /** @@ -1350,11 +1361,11 @@ private static boolean likeOrInOperator(String name) { * don't have write permission to the docs. *

*/ - private static void writeToTempDir(String subdir, String str, String extension) throws IOException { + private static void writeToTempDir(String subdir, String name, String extension, String str) throws IOException { // We have to write to a tempdir because it's all test are allowed to write to. Gradle can move them. Path dir = PathUtils.get(System.getProperty("java.io.tmpdir")).resolve("esql").resolve("functions").resolve(subdir); Files.createDirectories(dir); - Path file = dir.resolve(functionName() + "." + extension); + Path file = dir.resolve(name + "." + extension); Files.writeString(file, str); LogManager.getLogger(getTestClass()).info("Wrote to file: {}", file); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeTests.java index 589477a8bebdc..26340be224082 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/RLikeTests.java @@ -20,7 +20,9 @@ import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.junit.AfterClass; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.function.Function; @@ -150,4 +152,9 @@ static Expression buildRLike(Logger logger, Source source, List args ? new RLike(source, expression, new RLikePattern(patternString), true) : new RLike(source, expression, new RLikePattern(patternString)); } + + @AfterClass + public static void renderNotRLike() throws IOException { + WildcardLikeTests.renderNot(constructorWithFunctionInfo(RLike.class), "RLIKE", d -> d); + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLikeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLikeTests.java index e60c5f77ab42e..7f04f076ed15f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLikeTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLikeTests.java @@ -9,6 +9,7 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import com.unboundid.util.NotNull; import org.apache.lucene.util.BytesRef; import org.elasticsearch.xpack.esql.core.expression.Expression; @@ -18,11 +19,19 @@ import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; +import org.elasticsearch.xpack.esql.expression.function.Example; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.FunctionName; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.junit.AfterClass; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.function.Function; import java.util.function.Supplier; import static org.hamcrest.Matchers.equalTo; @@ -87,4 +96,67 @@ static Expression buildWildcardLike(Source source, List args) { } return new WildcardLike(source, expression, new WildcardPattern(((BytesRef) pattern.fold(FoldContext.small())).utf8ToString())); } + + @AfterClass + public static void renderNotLike() throws IOException { + renderNot(constructorWithFunctionInfo(WildcardLike.class), "LIKE", d -> d); + } + + public static void renderNot(@NotNull Constructor ctor, String name, Function description) throws IOException { + FunctionInfo orig = ctor.getAnnotation(FunctionInfo.class); + assert orig != null; + FunctionInfo functionInfo = new FunctionInfo() { + @Override + public Class annotationType() { + return orig.annotationType(); + } + + @Override + public String operator() { + return "NOT " + name; + } + + @Override + public String[] returnType() { + return orig.returnType(); + } + + @Override + public boolean preview() { + return orig.preview(); + } + + @Override + public String description() { + return description.apply(orig.description().replace(name, "NOT " + name)); + } + + @Override + public String detailedDescription() { + return ""; + } + + @Override + public String note() { + return orig.note().replace(name, "NOT " + name); + } + + @Override + public String appendix() { + return orig.appendix().replace(name, "NOT " + name); + } + + @Override + public boolean isAggregation() { + return orig.isAggregation(); + } + + @Override + public Example[] examples() { + // throw away examples + return new Example[] {}; + } + }; + renderDocsForOperators("not_" + name.toLowerCase(Locale.ENGLISH), ctor, functionInfo); + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/InTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/InTests.java index 80f67ec8e5e3a..03a4b063d6294 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/InTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/comparison/InTests.java @@ -19,7 +19,10 @@ import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.scalar.string.WildcardLikeTests; +import org.junit.AfterClass; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -333,4 +336,14 @@ private static void bytesRefs(List suppliers, int items) { protected Expression build(Source source, List args) { return new In(source, args.get(args.size() - 1), args.subList(0, args.size() - 1)); } + + @AfterClass + public static void renderNotIn() throws IOException { + WildcardLikeTests.renderNot( + constructorWithFunctionInfo(In.class), + "IN", + d -> "The `NOT IN` operator allows testing whether a field or expression does *not* equal any element " + + "in a list of literals, fields or expressions." + ); + } }