diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/url_encode.md b/docs/reference/query-languages/esql/_snippets/functions/description/url_encode.md
new file mode 100644
index 0000000000000..37fec2050f8b8
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/description/url_encode.md
@@ -0,0 +1,6 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Description**
+
+URL encodes the input.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode.md b/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode.md
new file mode 100644
index 0000000000000..beca9c93c767c
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode.md
@@ -0,0 +1,13 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Example**
+
+```esql
+ROW u = "https://www.example.com/papers?q=information+retrieval&year=2024&citations=high" | EVAL u = URL_ENCODE(u)
+```
+
+| u:keyword |
+| --- |
+| https%3A%2F%2Fwww.example.com%2Fpapers%3Fq%3Dinformation%2Bretrieval%26year%3D2024%26citations%3Dhigh |
+
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/url_encode.md b/docs/reference/query-languages/esql/_snippets/functions/layout/url_encode.md
new file mode 100644
index 0000000000000..1a186a440b436
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/layout/url_encode.md
@@ -0,0 +1,27 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+## `URL_ENCODE` [esql-url_encode]
+```{applies_to}
+stack: development
+serverless: preview
+```
+
+**Syntax**
+
+:::{image} ../../../images/functions/url_encode.svg
+:alt: Embedded
+:class: text-center
+:::
+
+
+:::{include} ../parameters/url_encode.md
+:::
+
+:::{include} ../description/url_encode.md
+:::
+
+:::{include} ../types/url_encode.md
+:::
+
+:::{include} ../examples/url_encode.md
+:::
diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode.md
new file mode 100644
index 0000000000000..65b5738d3d625
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode.md
@@ -0,0 +1,7 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Parameters**
+
+`string`
+: URL to encode.
+
diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/url_encode.md b/docs/reference/query-languages/esql/_snippets/functions/types/url_encode.md
new file mode 100644
index 0000000000000..7221b9139e2b8
--- /dev/null
+++ b/docs/reference/query-languages/esql/_snippets/functions/types/url_encode.md
@@ -0,0 +1,9 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+**Supported types**
+
+| string | result |
+| --- | --- |
+| keyword | keyword |
+| text | keyword |
+
diff --git a/docs/reference/query-languages/esql/images/functions/url_encode.svg b/docs/reference/query-languages/esql/images/functions/url_encode.svg
new file mode 100644
index 0000000000000..f046a7769e9a5
--- /dev/null
+++ b/docs/reference/query-languages/esql/images/functions/url_encode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/url_encode.json b/docs/reference/query-languages/esql/kibana/definition/functions/url_encode.json
new file mode 100644
index 0000000000000..85197c6a23b61
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/definition/functions/url_encode.json
@@ -0,0 +1,37 @@
+{
+ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.",
+ "type" : "scalar",
+ "name" : "url_encode",
+ "description" : "URL encodes the input.",
+ "signatures" : [
+ {
+ "params" : [
+ {
+ "name" : "string",
+ "type" : "keyword",
+ "optional" : false,
+ "description" : "URL to encode."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ },
+ {
+ "params" : [
+ {
+ "name" : "string",
+ "type" : "text",
+ "optional" : false,
+ "description" : "URL to encode."
+ }
+ ],
+ "variadic" : false,
+ "returnType" : "keyword"
+ }
+ ],
+ "examples" : [
+ "ROW u = \"https://www.example.com/papers?q=information+retrieval&year=2024&citations=high\" | EVAL u = URL_ENCODE(u)"
+ ],
+ "preview" : true,
+ "snapshot_only" : true
+}
diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/url_encode.md b/docs/reference/query-languages/esql/kibana/docs/functions/url_encode.md
new file mode 100644
index 0000000000000..c882d55034443
--- /dev/null
+++ b/docs/reference/query-languages/esql/kibana/docs/functions/url_encode.md
@@ -0,0 +1,8 @@
+% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.
+
+### URL ENCODE
+URL encodes the input.
+
+```esql
+ROW u = "https://www.example.com/papers?q=information+retrieval&year=2024&citations=high" | EVAL u = URL_ENCODE(u)
+```
diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec
index 7110b299e13a2..d89f5a52f9899 100644
--- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec
+++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec
@@ -2465,3 +2465,39 @@ warning:Line 2:9: java.lang.IllegalArgumentException: single-value function enco
@timestamp:date | message:text
2023-10-23T13:55:01.544Z|Connected to 10.1.0.1
;
+
+url_encode sample for docs
+required_capability: url_encode
+
+// tag::url_encode[]
+ROW u = "https://www.example.com/papers?q=information+retrieval&year=2024&citations=high" | EVAL u = URL_ENCODE(u)
+// end::url_encode[]
+;
+
+// tag::url_encode-result[]
+u:keyword
+https%3A%2F%2Fwww.example.com%2Fpapers%3Fq%3Dinformation%2Bretrieval%26year%3D2024%26citations%3Dhigh
+// end::url_encode-result[]
+;
+
+url_encode mixed functions tests
+required_capability: url_encode
+
+FROM employees
+| WHERE emp_no == 10001
+| EVAL a = TRIM(URL_ENCODE(first_name))
+| EVAL b = URL_ENCODE(TO_LOWER(first_name))
+| KEEP a,b;
+
+a:keyword | b:keyword
+Georgi | georgi
+;
+
+url_encode mixed input tests
+required_capability: url_encode
+
+ROW u = ["hello elastic!", "a+b-c%d", "", "!#$&'()*+,/:;=?@[]"] | EVAL u = URL_ENCODE(u);
+
+u:keyword
+["hello+elastic%21", "a%2Bb-c%25d", "", "%21%23%24%26%27%28%29*%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"]
+;
diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeEvaluator.java
new file mode 100644
index 0000000000000..17fbcb4d462c9
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeEvaluator.java
@@ -0,0 +1,153 @@
+// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+// or more contributor license agreements. Licensed under the Elastic License
+// 2.0; you may not use this file except in compliance with the Elastic License
+// 2.0.
+package org.elasticsearch.xpack.esql.expression.function.scalar.convert;
+
+import java.lang.Override;
+import java.lang.String;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.IntVector;
+import org.elasticsearch.compute.data.OrdinalBytesRefVector;
+import org.elasticsearch.compute.data.Vector;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link UrlEncode}.
+ * This class is generated. Edit {@code ConvertEvaluatorImplementer} instead.
+ */
+public final class UrlEncodeEvaluator extends AbstractConvertFunction.AbstractEvaluator {
+ private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(UrlEncodeEvaluator.class);
+
+ private final EvalOperator.ExpressionEvaluator val;
+
+ public UrlEncodeEvaluator(Source source, EvalOperator.ExpressionEvaluator val,
+ DriverContext driverContext) {
+ super(driverContext, source);
+ this.val = val;
+ }
+
+ @Override
+ public EvalOperator.ExpressionEvaluator next() {
+ return val;
+ }
+
+ @Override
+ public Block evalVector(Vector v) {
+ BytesRefVector vector = (BytesRefVector) v;
+ OrdinalBytesRefVector ordinals = vector.asOrdinals();
+ if (ordinals != null) {
+ return evalOrdinals(ordinals);
+ }
+ int positionCount = v.getPositionCount();
+ BytesRef scratchPad = new BytesRef();
+ if (vector.isConstant()) {
+ return driverContext.blockFactory().newConstantBytesRefBlockWith(evalValue(vector, 0, scratchPad), positionCount);
+ }
+ try (BytesRefBlock.Builder builder = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ for (int p = 0; p < positionCount; p++) {
+ builder.appendBytesRef(evalValue(vector, p, scratchPad));
+ }
+ return builder.build();
+ }
+ }
+
+ private BytesRef evalValue(BytesRefVector container, int index, BytesRef scratchPad) {
+ BytesRef value = container.getBytesRef(index, scratchPad);
+ return UrlEncode.process(value);
+ }
+
+ @Override
+ public Block evalBlock(Block b) {
+ BytesRefBlock block = (BytesRefBlock) b;
+ int positionCount = block.getPositionCount();
+ try (BytesRefBlock.Builder builder = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+ BytesRef scratchPad = new BytesRef();
+ for (int p = 0; p < positionCount; p++) {
+ int valueCount = block.getValueCount(p);
+ int start = block.getFirstValueIndex(p);
+ int end = start + valueCount;
+ boolean positionOpened = false;
+ boolean valuesAppended = false;
+ for (int i = start; i < end; i++) {
+ BytesRef value = evalValue(block, i, scratchPad);
+ if (positionOpened == false && valueCount > 1) {
+ builder.beginPositionEntry();
+ positionOpened = true;
+ }
+ builder.appendBytesRef(value);
+ valuesAppended = true;
+ }
+ if (valuesAppended == false) {
+ builder.appendNull();
+ } else if (positionOpened) {
+ builder.endPositionEntry();
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ private BytesRef evalValue(BytesRefBlock container, int index, BytesRef scratchPad) {
+ BytesRef value = container.getBytesRef(index, scratchPad);
+ return UrlEncode.process(value);
+ }
+
+ private Block evalOrdinals(OrdinalBytesRefVector v) {
+ int positionCount = v.getDictionaryVector().getPositionCount();
+ BytesRef scratchPad = new BytesRef();
+ try (BytesRefVector.Builder builder = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) {
+ for (int p = 0; p < positionCount; p++) {
+ builder.appendBytesRef(evalValue(v.getDictionaryVector(), p, scratchPad));
+ }
+ IntVector ordinals = v.getOrdinalsVector();
+ ordinals.incRef();
+ return new OrdinalBytesRefVector(ordinals, builder.build()).asBlock();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "UrlEncodeEvaluator[" + "val=" + val + "]";
+ }
+
+ @Override
+ public void close() {
+ Releasables.closeExpectNoException(val);
+ }
+
+ @Override
+ public long baseRamBytesUsed() {
+ long baseRamBytesUsed = BASE_RAM_BYTES_USED;
+ baseRamBytesUsed += val.baseRamBytesUsed();
+ return baseRamBytesUsed;
+ }
+
+ public static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+ private final Source source;
+
+ private final EvalOperator.ExpressionEvaluator.Factory val;
+
+ public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val) {
+ this.source = source;
+ this.val = val;
+ }
+
+ @Override
+ public UrlEncodeEvaluator get(DriverContext context) {
+ return new UrlEncodeEvaluator(source, val.get(context), context);
+ }
+
+ @Override
+ public String toString() {
+ return "UrlEncodeEvaluator[" + "val=" + val + "]";
+ }
+ }
+}
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
index e58fa2eeb5856..b39135097f1f9 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
@@ -1399,7 +1399,12 @@ public enum Cap {
/**
* Allow qualifiers in attribute names.
*/
- NAME_QUALIFIERS(Build.current().isSnapshot());
+ NAME_QUALIFIERS(Build.current().isSnapshot()),
+
+ /**
+ * URL encoding function.
+ */
+ URL_ENCODE(Build.current().isSnapshot());
private final boolean enabled;
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
index 311f666581279..25d131b23f623 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/ExpressionWritables.java
@@ -34,6 +34,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToString;
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToUnsignedLong;
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToVersion;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlEncode;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Abs;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Acos;
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Asin;
@@ -223,6 +224,7 @@ public static List unaryScalars() {
entries.add(WildcardLike.ENTRY);
entries.add(WildcardLikeList.ENTRY);
entries.add(Delay.ENTRY);
+ entries.add(UrlEncode.ENTRY);
// mv functions
entries.addAll(MvFunctionWritables.getNamedWriteables());
return entries;
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
index b47ca9fa8d4d8..968a20345cdbc 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
@@ -83,6 +83,7 @@
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToTimeDuration;
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToUnsignedLong;
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToVersion;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlEncode;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateDiff;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateExtract;
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateFormat;
@@ -513,7 +514,8 @@ private static FunctionDefinition[][] snapshotFunctions() {
def(L1Norm.class, L1Norm::new, "v_l1_norm"),
def(L2Norm.class, L2Norm::new, "v_l2_norm"),
def(Magnitude.class, Magnitude::new, "v_magnitude"),
- def(Hamming.class, Hamming::new, "v_hamming") } };
+ def(Hamming.class, Hamming::new, "v_hamming"),
+ def(UrlEncode.class, UrlEncode::new, "url_encode") } };
}
public EsqlFunctionRegistry snapshotRegistry() {
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncode.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncode.java
new file mode 100644
index 0000000000000..0a6196197233c
--- /dev/null
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncode.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.convert;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.compute.ann.ConvertEvaluator;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
+import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo;
+import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle;
+import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
+import org.elasticsearch.xpack.esql.expression.function.Param;
+import org.elasticsearch.xpack.esql.expression.function.scalar.UnaryScalarFunction;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString;
+
+public final class UrlEncode extends UnaryScalarFunction {
+
+ public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
+ Expression.class,
+ "UrlEncode",
+ UrlEncode::new
+ );
+
+ private UrlEncode(StreamInput in) throws IOException {
+ super(in);
+ }
+
+ @FunctionInfo(
+ returnType = "keyword",
+ preview = true,
+ description = "URL encodes the input.",
+ examples = { @Example(file = "string", tag = "url_encode") },
+ appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.DEVELOPMENT) }
+ )
+ public UrlEncode(Source source, @Param(name = "string", type = { "keyword", "text" }, description = "URL to encode.") Expression str) {
+ super(source, str);
+ }
+
+ @Override
+ public Expression replaceChildren(List newChildren) {
+ return new UrlEncode(source(), newChildren.get(0));
+ }
+
+ @Override
+ protected NodeInfo extends Expression> info() {
+ return NodeInfo.create(this, UrlEncode::new, field());
+ }
+
+ @Override
+ public String getWriteableName() {
+ return ENTRY.name;
+ }
+
+ @Override
+ protected TypeResolution resolveType() {
+ if (childrenResolved() == false) {
+ return new TypeResolution("Unresolved children");
+ }
+ return isString(field, sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+ }
+
+ @Override
+ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
+ return new UrlEncodeEvaluator.Factory(source(), toEvaluator.apply(field()));
+ }
+
+ @ConvertEvaluator()
+ static BytesRef process(final BytesRef val) {
+ String input = val.utf8ToString();
+ String encoded = URLEncoder.encode(input, StandardCharsets.UTF_8);
+ return new BytesRef(encoded);
+ }
+
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeErrorTests.java
new file mode 100644
index 0000000000000..38d00f3cf8c2c
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeErrorTests.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.convert;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.core.type.DataType;
+import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.hamcrest.Matcher;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class UrlEncodeErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
+ @Override
+ protected List cases() {
+ return paramsToSuppliers(UrlEncodeTests.parameters());
+ }
+
+ @Override
+ protected Expression build(Source source, List args) {
+ return new UrlEncode(source, args.get(0));
+ }
+
+ @Override
+ protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) {
+ return equalTo(typeErrorMessage(false, validPerPosition, signature, (v, p) -> "string"));
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeSerializationTests.java
new file mode 100644
index 0000000000000..94c0fd479fb04
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeSerializationTests.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.convert;
+
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+import org.elasticsearch.xpack.esql.core.tree.Source;
+import org.elasticsearch.xpack.esql.expression.AbstractUnaryScalarSerializationTests;
+
+public class UrlEncodeSerializationTests extends AbstractUnaryScalarSerializationTests {
+ @Override
+ protected UrlEncode create(Source source, Expression child) {
+ return new UrlEncode(source, child);
+ }
+}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeTests.java
new file mode 100644
index 0000000000000..c09a44d4af572
--- /dev/null
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeTests.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.esql.expression.function.scalar.convert;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.xpack.esql.core.expression.Expression;
+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.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+@FunctionName("url_encode")
+public class UrlEncodeTests extends AbstractScalarFunctionTestCase {
+
+ private record RandomUrl(String plain, String encoded) {}
+
+ public UrlEncodeTests(@Name("TestCase") Supplier testCaseSupplier) {
+ this.testCase = testCaseSupplier.get();
+ }
+
+ @ParametersFactory
+ public static Iterable