Browse Source

ESQL: Signatures for `NOT IN` et al (#120673)

* ESQL: Signatures for `NOT IN` et al

This generates signatures for `NOT IN`, `NOT LIKE`, and `NOT RLIKE`
using a small hack on top of the process used to generate the signatures
for `IN`, `LIKE`, and `RLIKE`. This is a very perl-worth hack, replacing
`LIKE` with `NOT LIKE` in the description. But it's useful for our
kibana friends and if we need to make it nicer we can do so later.

* Zap
Nik Everett 8 months ago
parent
commit
eae93a2097

+ 263 - 0
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
+}

+ 47 - 0
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
+}

+ 47 - 0
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<<regexp-syntax,regular expressions>>. `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
+}

+ 7 - 0
docs/reference/esql/functions/kibana/docs/not_in.md

@@ -0,0 +1,7 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### 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.
+

+ 15 - 0
docs/reference/esql/functions/kibana/docs/not_like.md

@@ -0,0 +1,15 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### 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.
+

+ 10 - 0
docs/reference/esql/functions/kibana/docs/not_rlike.md

@@ -0,0 +1,10 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### NOT_RLIKE
+Use `NOT RLIKE` to filter data based on string patterns using using
+<<regexp-syntax,regular expressions>>. `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.
+

+ 22 - 0
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
+|===

+ 10 - 0
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
+|===

+ 10 - 0
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
+|===

+ 45 - 34
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java

@@ -823,12 +823,13 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
 
     @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 abstract class AbstractFunctionTestCase extends ESTestCase {
                     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 abstract class AbstractFunctionTestCase extends ESTestCase {
             + "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<EsqlFunctionRegistry.ArgSignature> args) throws IOException {
+    private static void renderTypes(String name, List<EsqlFunctionRegistry.ArgSignature> args) throws IOException {
         StringBuilder header = new StringBuilder();
         List<String> argNames = args.stream().map(EsqlFunctionRegistry.ArgSignature::name).toList();
         for (String arg : argNames) {
@@ -984,11 +988,11 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
             [%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<String> argNames, List<String> argDescriptions) throws IOException {
+    private static void renderParametersList(String name, List<String> argNames, List<String> argDescriptions) throws IOException {
         StringBuilder builder = new StringBuilder();
         builder.append(DOCS_WARNING);
         builder.append("*Parameters*\n");
@@ -996,11 +1000,11 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
             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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         }
         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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
             }
         }
         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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
             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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
      * 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 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
      * don't have write permission to the docs.
      * </p>
      */
-    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);
     }

+ 7 - 0
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.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.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 @@ public class RLikeTests extends AbstractScalarFunctionTestCase {
             ? 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);
+    }
 }

+ 72 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/WildcardLikeTests.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.esql.expression.function.scalar.string;
 
 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.expression.predicate.regex.WildcardPatt
 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 @@ public class WildcardLikeTests extends AbstractScalarFunctionTestCase {
         }
         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<String, String> description) throws IOException {
+        FunctionInfo orig = ctor.getAnnotation(FunctionInfo.class);
+        assert orig != null;
+        FunctionInfo functionInfo = new FunctionInfo() {
+            @Override
+            public Class<? extends Annotation> 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);
+    }
 }

+ 13 - 0
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.tree.Source;
 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 @@ public class InTests extends AbstractFunctionTestCase {
     protected Expression build(Source source, List<Expression> 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."
+        );
+    }
 }