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 @@ +URL_ENCODE(string) \ 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 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 parameters() { + List suppliers = new ArrayList<>(); + + for (DataType dataType : DataType.stringTypes()) { + suppliers.add(new TestCaseSupplier(List.of(dataType), () -> createTestCaseWithRandomUrl(dataType))); + + for (TestCaseSupplier.TypedDataSupplier supplier : TestCaseSupplier.stringCases(dataType)) { + TestCaseSupplier testCaseSupplier = new TestCaseSupplier( + supplier.name(), + List.of(supplier.type()), + () -> createTestCaseWithRandomString(dataType, supplier) + ); + suppliers.add(testCaseSupplier); + } + } + + return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(false, suppliers); + } + + @Override + protected Expression build(Source source, List args) { + return new UrlEncode(source, args.get(0)); + } + + private static TestCaseSupplier.TestCase createTestCaseWithRandomUrl(DataType dataType) { + RandomUrl url = generateRandomUrl(); + BytesRef input = new BytesRef(url.plain()); + BytesRef output = new BytesRef(url.encoded()); + TestCaseSupplier.TypedData fieldTypedData = new TestCaseSupplier.TypedData(input, dataType, "string"); + + return new TestCaseSupplier.TestCase( + List.of(fieldTypedData), + "UrlEncodeEvaluator[val=Attribute[channel=0]]", + dataType, + equalTo(output) + ); + } + + private static TestCaseSupplier.TestCase createTestCaseWithRandomString( + DataType dataType, + TestCaseSupplier.TypedDataSupplier supplier + ) { + TestCaseSupplier.TypedData fieldTypedData = supplier.get(); + BytesRef input = BytesRefs.toBytesRef(fieldTypedData.data()); + BytesRef output = new BytesRef(URLEncoder.encode(input.utf8ToString(), StandardCharsets.UTF_8)); + + return new TestCaseSupplier.TestCase( + List.of(fieldTypedData), + "UrlEncodeEvaluator[val=Attribute[channel=0]]", + dataType, + equalTo(output) + ); + } + + private static RandomUrl generateRandomUrl() { + String protocol = randomFrom("http://", "https://", ""); + String domain = String.format(Locale.ROOT, "%s.com", randomAlphaOfLengthBetween(3, 10)); + String path = randomFrom("", "/" + randomAlphanumericOfLength(5) + "/"); + String query = randomFrom("", "?" + randomAlphaOfLength(5) + "=" + randomAlphanumericOfLength(5)); + + String plain = String.format(Locale.ROOT, "%s%s%s%s", protocol, domain, path, query); + String encoded = URLEncoder.encode(plain, StandardCharsets.UTF_8); + + return new RandomUrl(plain, encoded); + } +}