From 247f876bc4a2f0ed7af1a38ae0787ff041282840 Mon Sep 17 00:00:00 2001 From: Anh Nguyen Date: Wed, 8 Oct 2025 01:26:07 -0400 Subject: [PATCH] Mirror upstream elastic/elasticsearch#134503 as single snapshot commit for AI review BASE=62e53cd13c8846f0729390450a5cfbd0304e7f40 HEAD=6b2e950488ed317736d19f100fbcf58bb587fea4 Branch=main --- .../functions/description/url_decode.md | 2 +- .../functions/description/url_encode.md | 2 +- .../description/url_encode_component.md | 6 + .../functions/examples/url_decode.md | 5 +- .../functions/examples/url_encode.md | 4 +- .../examples/url_encode_component.md | 14 ++ .../functions/layout/url_encode_component.md | 27 +++ .../functions/parameters/url_decode.md | 2 +- .../functions/parameters/url_encode.md | 2 +- .../parameters/url_encode_component.md | 7 + .../functions/types/url_encode_component.md | 9 + .../images/functions/url_encode_component.svg | 1 + .../definition/functions/url_decode.json | 8 +- .../definition/functions/url_encode.json | 8 +- .../functions/url_encode_component.json | 37 +++ .../esql/kibana/docs/functions/url_decode.md | 5 +- .../esql/kibana/docs/functions/url_encode.md | 4 +- .../docs/functions/url_encode_component.md | 9 + .../src/main/resources/string.csv-spec | 73 +++++- .../scalar/convert/UrlDecodeEvaluator.java | 50 ++-- .../convert/UrlEncodeComponentEvaluator.java | 162 +++++++++++++ .../scalar/convert/UrlEncodeEvaluator.java | 21 +- .../xpack/esql/action/EsqlCapabilities.java | 5 + .../esql/expression/ExpressionWritables.java | 2 + .../function/EsqlFunctionRegistry.java | 2 + .../function/scalar/convert/UrlDecode.java | 6 +- .../function/scalar/convert/UrlEncode.java | 26 +- .../scalar/convert/UrlEncodeComponent.java | 99 ++++++++ .../function/scalar/util/UrlCodecUtils.java | 116 +++++++++ .../AbstractUrlEncodeDecodeTestCase.java | 222 +++++++++++++++--- .../scalar/convert/UrlDecodeTests.java | 2 +- .../convert/UrlEncodeComponentErrorTests.java | 37 +++ .../UrlEncodeComponentSerializationTests.java | 21 ++ .../convert/UrlEncodeComponentTests.java | 37 +++ .../scalar/convert/UrlEncodeTests.java | 2 +- 35 files changed, 933 insertions(+), 102 deletions(-) create mode 100644 docs/reference/query-languages/esql/_snippets/functions/description/url_encode_component.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/examples/url_encode_component.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/layout/url_encode_component.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode_component.md create mode 100644 docs/reference/query-languages/esql/_snippets/functions/types/url_encode_component.md create mode 100644 docs/reference/query-languages/esql/images/functions/url_encode_component.svg create mode 100644 docs/reference/query-languages/esql/kibana/definition/functions/url_encode_component.json create mode 100644 docs/reference/query-languages/esql/kibana/docs/functions/url_encode_component.md create mode 100644 x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentEvaluator.java create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponent.java create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/util/UrlCodecUtils.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentErrorTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentSerializationTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentTests.java diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/url_decode.md b/docs/reference/query-languages/esql/_snippets/functions/description/url_decode.md index 243c821782bd0..98c05e5888375 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/description/url_decode.md +++ b/docs/reference/query-languages/esql/_snippets/functions/description/url_decode.md @@ -2,5 +2,5 @@ **Description** -URL decodes the input. +URL-decodes the input, or returns `null` and adds a warning header to the response if the input cannot be decoded. 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 index 37fec2050f8b8..5c4eb5bd6d8ea 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/description/url_encode.md +++ b/docs/reference/query-languages/esql/_snippets/functions/description/url_encode.md @@ -2,5 +2,5 @@ **Description** -URL encodes the input. +URL-encodes the input. All characters are percent-encoded except for alphanumerics, `.`, `-`, `_`, and `~`. Spaces are encoded as `+`. diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/url_encode_component.md b/docs/reference/query-languages/esql/_snippets/functions/description/url_encode_component.md new file mode 100644 index 0000000000000..2947e86fcffae --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/description/url_encode_component.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. All characters are percent-encoded except for alphanumerics, `.`, `-`, `_`, and `~`. Spaces are encoded as `%20`. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/url_decode.md b/docs/reference/query-languages/esql/_snippets/functions/examples/url_decode.md index 7dcc495a42732..61790ceda15e2 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/examples/url_decode.md +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/url_decode.md @@ -3,11 +3,12 @@ **Example** ```esql -ROW u = "https%3A%2F%2Fwww.example.com%2Fpapers%3Fq%3Dinformation%2Bretrieval%26year%3D2024%26citations%3Dhigh" | EVAL u = URL_DECODE(u) +ROW u = "https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo%20bar%26y%3Dbaz" +| EVAL u = URL_DECODE(u) ``` | u:keyword | | --- | -| https://www.example.com/papers?q=information+retrieval&year=2024&citations=high | +| https://example.com/?x=foo bar&y=baz | 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 index beca9c93c767c..e6a3186eb01b8 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode.md +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode.md @@ -3,11 +3,11 @@ **Example** ```esql -ROW u = "https://www.example.com/papers?q=information+retrieval&year=2024&citations=high" | EVAL u = URL_ENCODE(u) +ROW u = "https://example.com/?x=foo bar&y=baz" | EVAL u = URL_ENCODE(u) ``` | u:keyword | | --- | -| https%3A%2F%2Fwww.example.com%2Fpapers%3Fq%3Dinformation%2Bretrieval%26year%3D2024%26citations%3Dhigh | +| https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo+bar%26y%3Dbaz | diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode_component.md b/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode_component.md new file mode 100644 index 0000000000000..2dbe5f62306a4 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/url_encode_component.md @@ -0,0 +1,14 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Example** + +```esql +ROW u = "https://example.com/?x=foo bar&y=baz" +| EVAL u = URL_ENCODE_COMPONENT(u) +``` + +| u:keyword | +| --- | +| https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo%20bar%26y%3Dbaz | + + diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/url_encode_component.md b/docs/reference/query-languages/esql/_snippets/functions/layout/url_encode_component.md new file mode 100644 index 0000000000000..a358bcff3b8a9 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/url_encode_component.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_COMPONENT` [esql-url_encode_component] +```{applies_to} +stack: development +serverless: preview +``` + +**Syntax** + +:::{image} ../../../images/functions/url_encode_component.svg +:alt: Embedded +:class: text-center +::: + + +:::{include} ../parameters/url_encode_component.md +::: + +:::{include} ../description/url_encode_component.md +::: + +:::{include} ../types/url_encode_component.md +::: + +:::{include} ../examples/url_encode_component.md +::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/url_decode.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/url_decode.md index e69055d67ec54..d470937892079 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/parameters/url_decode.md +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/url_decode.md @@ -3,5 +3,5 @@ **Parameters** `string` -: URL encoded string to decode. +: The URL-encoded string to decode. 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 index 65b5738d3d625..ac0057f658fd2 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode.md +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode.md @@ -3,5 +3,5 @@ **Parameters** `string` -: URL to encode. +: The URL to encode. diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode_component.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode_component.md new file mode 100644 index 0000000000000..ac0057f658fd2 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/url_encode_component.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` +: The URL to encode. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/url_encode_component.md b/docs/reference/query-languages/esql/_snippets/functions/types/url_encode_component.md new file mode 100644 index 0000000000000..7221b9139e2b8 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/types/url_encode_component.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_component.svg b/docs/reference/query-languages/esql/images/functions/url_encode_component.svg new file mode 100644 index 0000000000000..a54ff1783df14 --- /dev/null +++ b/docs/reference/query-languages/esql/images/functions/url_encode_component.svg @@ -0,0 +1 @@ +URL_ENCODE_COMPONENT(string) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/url_decode.json b/docs/reference/query-languages/esql/kibana/definition/functions/url_decode.json index 44dcef86adcb4..59b7a03354695 100644 --- a/docs/reference/query-languages/esql/kibana/definition/functions/url_decode.json +++ b/docs/reference/query-languages/esql/kibana/definition/functions/url_decode.json @@ -2,7 +2,7 @@ "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.", "type" : "scalar", "name" : "url_decode", - "description" : "URL decodes the input.", + "description" : "URL-decodes the input, or returns `null` and adds a warning header to the response if the input cannot be decoded.", "signatures" : [ { "params" : [ @@ -10,7 +10,7 @@ "name" : "string", "type" : "keyword", "optional" : false, - "description" : "URL encoded string to decode." + "description" : "The URL-encoded string to decode." } ], "variadic" : false, @@ -22,7 +22,7 @@ "name" : "string", "type" : "text", "optional" : false, - "description" : "URL encoded string to decode." + "description" : "The URL-encoded string to decode." } ], "variadic" : false, @@ -30,7 +30,7 @@ } ], "examples" : [ - "ROW u = \"https%3A%2F%2Fwww.example.com%2Fpapers%3Fq%3Dinformation%2Bretrieval%26year%3D2024%26citations%3Dhigh\" | EVAL u = URL_DECODE(u)" + "ROW u = \"https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo%20bar%26y%3Dbaz\"\n| EVAL u = URL_DECODE(u)" ], "preview" : true, "snapshot_only" : true 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 index 85197c6a23b61..0b8e8e162dc62 100644 --- a/docs/reference/query-languages/esql/kibana/definition/functions/url_encode.json +++ b/docs/reference/query-languages/esql/kibana/definition/functions/url_encode.json @@ -2,7 +2,7 @@ "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.", + "description" : "URL-encodes the input. All characters are percent-encoded except for alphanumerics, `.`, `-`, `_`, and `~`. Spaces are encoded as `+`.", "signatures" : [ { "params" : [ @@ -10,7 +10,7 @@ "name" : "string", "type" : "keyword", "optional" : false, - "description" : "URL to encode." + "description" : "The URL to encode." } ], "variadic" : false, @@ -22,7 +22,7 @@ "name" : "string", "type" : "text", "optional" : false, - "description" : "URL to encode." + "description" : "The URL to encode." } ], "variadic" : false, @@ -30,7 +30,7 @@ } ], "examples" : [ - "ROW u = \"https://www.example.com/papers?q=information+retrieval&year=2024&citations=high\" | EVAL u = URL_ENCODE(u)" + "ROW u = \"https://example.com/?x=foo bar&y=baz\" | EVAL u = URL_ENCODE(u)" ], "preview" : true, "snapshot_only" : true diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/url_encode_component.json b/docs/reference/query-languages/esql/kibana/definition/functions/url_encode_component.json new file mode 100644 index 0000000000000..fc52a12cba228 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/definition/functions/url_encode_component.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_component", + "description" : "URL-encodes the input. All characters are percent-encoded except for alphanumerics, `.`, `-`, `_`, and `~`. Spaces are encoded as `%20`.", + "signatures" : [ + { + "params" : [ + { + "name" : "string", + "type" : "keyword", + "optional" : false, + "description" : "The URL to encode." + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "string", + "type" : "text", + "optional" : false, + "description" : "The URL to encode." + } + ], + "variadic" : false, + "returnType" : "keyword" + } + ], + "examples" : [ + "ROW u = \"https://example.com/?x=foo bar&y=baz\"\n| EVAL u = URL_ENCODE_COMPONENT(u)" + ], + "preview" : true, + "snapshot_only" : true +} diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/url_decode.md b/docs/reference/query-languages/esql/kibana/docs/functions/url_decode.md index 41b423babbffd..28f411d9a6bc5 100644 --- a/docs/reference/query-languages/esql/kibana/docs/functions/url_decode.md +++ b/docs/reference/query-languages/esql/kibana/docs/functions/url_decode.md @@ -1,8 +1,9 @@ % This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. ### URL DECODE -URL decodes the input. +URL-decodes the input, or returns `null` and adds a warning header to the response if the input cannot be decoded. ```esql -ROW u = "https%3A%2F%2Fwww.example.com%2Fpapers%3Fq%3Dinformation%2Bretrieval%26year%3D2024%26citations%3Dhigh" | EVAL u = URL_DECODE(u) +ROW u = "https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo%20bar%26y%3Dbaz" +| EVAL u = URL_DECODE(u) ``` 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 index c882d55034443..3560902854a97 100644 --- a/docs/reference/query-languages/esql/kibana/docs/functions/url_encode.md +++ b/docs/reference/query-languages/esql/kibana/docs/functions/url_encode.md @@ -1,8 +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. +URL-encodes the input. All characters are percent-encoded except for alphanumerics, `.`, `-`, `_`, and `~`. Spaces are encoded as `+`. ```esql -ROW u = "https://www.example.com/papers?q=information+retrieval&year=2024&citations=high" | EVAL u = URL_ENCODE(u) +ROW u = "https://example.com/?x=foo bar&y=baz" | EVAL u = URL_ENCODE(u) ``` diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/url_encode_component.md b/docs/reference/query-languages/esql/kibana/docs/functions/url_encode_component.md new file mode 100644 index 0000000000000..9d8da3ea67f8b --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/docs/functions/url_encode_component.md @@ -0,0 +1,9 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +### URL ENCODE COMPONENT +URL-encodes the input. All characters are percent-encoded except for alphanumerics, `.`, `-`, `_`, and `~`. Spaces are encoded as `%20`. + +```esql +ROW u = "https://example.com/?x=foo bar&y=baz" +| EVAL u = URL_ENCODE_COMPONENT(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 49d3469aac41d..963a80cdf7a65 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 @@ -2583,13 +2583,13 @@ 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) +ROW u = "https://example.com/?x=foo bar&y=baz" | 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 +https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo+bar%26y%3Dbaz // end::url_encode-result[] ; @@ -2608,24 +2608,26 @@ Georgi | georgi url_encode mixed input tests required_capability: url_encode +required_capability: url_encode_component -ROW u = ["hello elastic!", "a+b-c%d", "", "!#$&'()*+,/:;=?@[]"] | EVAL u = URL_ENCODE(u); +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"] +["hello+elastic%21", "a%2Bb-c%25d", "", ".-_~", "%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D", "%F0%9F%94%A5%F0%9F%92%A7"] ; url_decode sample for docs required_capability: url_decode // tag::url_decode[] -ROW u = "https%3A%2F%2Fwww.example.com%2Fpapers%3Fq%3Dinformation%2Bretrieval%26year%3D2024%26citations%3Dhigh" | EVAL u = URL_DECODE(u) +ROW u = "https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo%20bar%26y%3Dbaz" +| EVAL u = URL_DECODE(u) // end::url_decode[] ; // tag::url_decode-result[] u:keyword -https://www.example.com/papers?q=information+retrieval&year=2024&citations=high +https://example.com/?x=foo bar&y=baz // end::url_decode-result[] ; @@ -2636,6 +2638,7 @@ FROM employees | WHERE emp_no == 10001 | EVAL a = TRIM(URL_DECODE(first_name)) | EVAL b = URL_DECODE(TO_LOWER(first_name)) +| EVAL c = URL_DECODE(TO_LOWER(first_name)) | KEEP a,b; a:keyword | b:keyword @@ -2651,6 +2654,19 @@ u:keyword "!#$&'()*+,/:;=?@[]" ; +url_decode bad input tests +required_capability: url_decode +required_capability: url_encode_component + +ROW u = "%#" | EVAL u = URL_DECODE(u); + +warning:Line 1:25: evaluation of [URL_DECODE(u)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 1:25: java.lang.IllegalArgumentException: URLDecoder: Incomplete trailing escape (%25) pattern + +u:keyword +null +; + combined url encode decode tests with table reads required_capability: url_encode required_capability: url_decode @@ -2684,3 +2700,48 @@ ROW u = ["https://www.example.com/papers?q=information+retrieval&year=2024&citat u:keyword ["https://www.example.com/papers?q=information+retrieval&year=2024&citations=high", "", "!#$&'()+/:;=?@[]", "πŸ’¨πŸ”₯πŸͺ¨πŸ’§"] ; + +url_encode_component sample for docs +required_capability: url_encode_component + +// tag::url_encode_component[] +ROW u = "https://example.com/?x=foo bar&y=baz" +| EVAL u = URL_ENCODE_COMPONENT(u) +// end::url_encode_component[] +; + +// tag::url_encode_component-result[] +u:keyword +https%3A%2F%2Fexample.com%2F%3Fx%3Dfoo%20bar%26y%3Dbaz +// end::url_encode_component-result[] +; + +url_encode_component special input tests +required_capability: url_encode_component +required_capability: url_decode + +ROW a = "+!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", b = ".-_~", c = "β—πŸΆπŸ±" +| EVAL a = URL_ENCODE_COMPONENT(a) +| EVAL b = URL_ENCODE_COMPONENT(b) +| EVAL c = URL_DECODE(URL_ENCODE_COMPONENT(c)); + +a:keyword | b:keyword | c:keyword +"%2B%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~" | ".-_~" |β—πŸΆπŸ± +; + +url_encode_component tests with table reads +required_capability: url_encode_component + +FROM books +| EVAL author_encoded = URL_ENCODE_COMPONENT(author), + title_encoded = URL_ENCODE_COMPONENT(title) +| KEEP book_no, author_encoded, title_encoded +| SORT book_no +| WHERE book_no IN ("1211", "1463") +; + +book_no:keyword | author_encoded:keyword | title_encoded:keyword +1211 | Fyodor%20Dostoevsky | The%20brothers%20Karamazov +1463 | J.%20R.%20R.%20Tolkien | Realms%20of%20Tolkien%3A%20Images%20of%20Middle-earth +; + diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeEvaluator.java index 10e073087dd3a..3712c9e6c44a2 100644 --- a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeEvaluator.java +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeEvaluator.java @@ -4,6 +4,7 @@ // 2.0. package org.elasticsearch.xpack.esql.expression.function.scalar.convert; +import java.lang.IllegalArgumentException; import java.lang.Override; import java.lang.String; import org.apache.lucene.util.BytesRef; @@ -11,8 +12,6 @@ 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; @@ -42,18 +41,24 @@ public EvalOperator.ExpressionEvaluator next() { @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 { + return driverContext.blockFactory().newConstantBytesRefBlockWith(evalValue(vector, 0, scratchPad), positionCount); + } catch (IllegalArgumentException e) { + registerException(e); + return driverContext.blockFactory().newConstantNullBlock(positionCount); + } } try (BytesRefBlock.Builder builder = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { for (int p = 0; p < positionCount; p++) { - builder.appendBytesRef(evalValue(vector, p, scratchPad)); + try { + builder.appendBytesRef(evalValue(vector, p, scratchPad)); + } catch (IllegalArgumentException e) { + registerException(e); + builder.appendNull(); + } } return builder.build(); } @@ -77,13 +82,17 @@ public Block evalBlock(Block b) { 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; + try { + BytesRef value = evalValue(block, i, scratchPad); + if (positionOpened == false && valueCount > 1) { + builder.beginPositionEntry(); + positionOpened = true; + } + builder.appendBytesRef(value); + valuesAppended = true; + } catch (IllegalArgumentException e) { + registerException(e); } - builder.appendBytesRef(value); - valuesAppended = true; } if (valuesAppended == false) { builder.appendNull(); @@ -100,19 +109,6 @@ private BytesRef evalValue(BytesRefBlock container, int index, BytesRef scratchP return UrlDecode.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 "UrlDecodeEvaluator[" + "val=" + val + "]"; diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentEvaluator.java new file mode 100644 index 0000000000000..0758fee9dee16 --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentEvaluator.java @@ -0,0 +1,162 @@ +// 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 java.util.function.Function; +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.BreakingBytesRefBuilder; +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 UrlEncodeComponent}. + * This class is generated. Edit {@code ConvertEvaluatorImplementer} instead. + */ +public final class UrlEncodeComponentEvaluator extends AbstractConvertFunction.AbstractEvaluator { + private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(UrlEncodeComponentEvaluator.class); + + private final EvalOperator.ExpressionEvaluator val; + + private final BreakingBytesRefBuilder scratch; + + public UrlEncodeComponentEvaluator(Source source, EvalOperator.ExpressionEvaluator val, + BreakingBytesRefBuilder scratch, DriverContext driverContext) { + super(driverContext, source); + this.val = val; + this.scratch = scratch; + } + + @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 UrlEncodeComponent.process(value, this.scratch); + } + + @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 UrlEncodeComponent.process(value, this.scratch); + } + + 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 "UrlEncodeComponentEvaluator[" + "val=" + val + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(val, scratch); + } + + @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; + + private final Function scratch; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, + Function scratch) { + this.source = source; + this.val = val; + this.scratch = scratch; + } + + @Override + public UrlEncodeComponentEvaluator get(DriverContext context) { + return new UrlEncodeComponentEvaluator(source, val.get(context), scratch.apply(context), context); + } + + @Override + public String toString() { + return "UrlEncodeComponentEvaluator[" + "val=" + val + "]"; + } + } +} 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 index 17fbcb4d462c9..995af91f5a17f 100644 --- 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 @@ -6,6 +6,7 @@ import java.lang.Override; import java.lang.String; +import java.util.function.Function; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.RamUsageEstimator; import org.elasticsearch.compute.data.Block; @@ -14,6 +15,7 @@ import org.elasticsearch.compute.data.IntVector; import org.elasticsearch.compute.data.OrdinalBytesRefVector; import org.elasticsearch.compute.data.Vector; +import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.core.Releasables; @@ -28,10 +30,13 @@ public final class UrlEncodeEvaluator extends AbstractConvertFunction.AbstractEv private final EvalOperator.ExpressionEvaluator val; + private final BreakingBytesRefBuilder scratch; + public UrlEncodeEvaluator(Source source, EvalOperator.ExpressionEvaluator val, - DriverContext driverContext) { + BreakingBytesRefBuilder scratch, DriverContext driverContext) { super(driverContext, source); this.val = val; + this.scratch = scratch; } @Override @@ -61,7 +66,7 @@ public Block evalVector(Vector v) { private BytesRef evalValue(BytesRefVector container, int index, BytesRef scratchPad) { BytesRef value = container.getBytesRef(index, scratchPad); - return UrlEncode.process(value); + return UrlEncode.process(value, this.scratch); } @Override @@ -97,7 +102,7 @@ public Block evalBlock(Block b) { private BytesRef evalValue(BytesRefBlock container, int index, BytesRef scratchPad) { BytesRef value = container.getBytesRef(index, scratchPad); - return UrlEncode.process(value); + return UrlEncode.process(value, this.scratch); } private Block evalOrdinals(OrdinalBytesRefVector v) { @@ -120,7 +125,7 @@ public String toString() { @Override public void close() { - Releasables.closeExpectNoException(val); + Releasables.closeExpectNoException(val, scratch); } @Override @@ -135,14 +140,18 @@ public static class Factory implements EvalOperator.ExpressionEvaluator.Factory private final EvalOperator.ExpressionEvaluator.Factory val; - public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val) { + private final Function scratch; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, + Function scratch) { this.source = source; this.val = val; + this.scratch = scratch; } @Override public UrlEncodeEvaluator get(DriverContext context) { - return new UrlEncodeEvaluator(source, val.get(context), context); + return new UrlEncodeEvaluator(source, val.get(context), scratch.apply(context), context); } @Override 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 6082b5e6e2576..0d7b7eb456cea 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 @@ -1460,6 +1460,11 @@ public enum Cap { */ URL_ENCODE(Build.current().isSnapshot()), + /** + * URL component encoding function. + */ + URL_ENCODE_COMPONENT(Build.current().isSnapshot()), + /** * URL decoding function. */ 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 2e06db66a85e5..c14e8168884a9 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 @@ -41,6 +41,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToVersion; import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlDecode; import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlEncode; +import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlEncodeComponent; 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; @@ -231,6 +232,7 @@ public static List unaryScalars() { entries.add(WildcardLikeList.ENTRY); entries.add(Delay.ENTRY); entries.add(UrlEncode.ENTRY); + entries.add(UrlEncodeComponent.ENTRY); entries.add(UrlDecode.ENTRY); // mv functions entries.addAll(MvFunctionWritables.getNamedWriteables()); 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 89ce24bd779b4..a08ba6123a794 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 @@ -96,6 +96,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToVersion; import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlDecode; import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlEncode; +import org.elasticsearch.xpack.esql.expression.function.scalar.convert.UrlEncodeComponent; 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; @@ -541,6 +542,7 @@ private static FunctionDefinition[][] snapshotFunctions() { def(Magnitude.class, Magnitude::new, "v_magnitude"), def(Hamming.class, Hamming::new, "v_hamming"), def(UrlEncode.class, UrlEncode::new, "url_encode"), + def(UrlEncodeComponent.class, UrlEncodeComponent::new, "url_encode_component"), def(UrlDecode.class, UrlDecode::new, "url_decode") } }; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecode.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecode.java index 1036811e9e839..80f55574ac923 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecode.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecode.java @@ -44,13 +44,13 @@ private UrlDecode(StreamInput in) throws IOException { @FunctionInfo( returnType = "keyword", preview = true, - description = "URL decodes the input.", + description = "URL-decodes the input, or returns `null` and adds a warning header to the response if the input cannot be decoded.", examples = { @Example(file = "string", tag = "url_decode") }, appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.DEVELOPMENT) } ) public UrlDecode( Source source, - @Param(name = "string", type = { "keyword", "text" }, description = "URL encoded string to decode.") Expression str + @Param(name = "string", type = { "keyword", "text" }, description = "The URL-encoded string to decode.") Expression str ) { super(source, str); } @@ -83,7 +83,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua return new UrlDecodeEvaluator.Factory(source(), toEvaluator.apply(field())); } - @ConvertEvaluator() + @ConvertEvaluator(warnExceptions = { IllegalArgumentException.class }) static BytesRef process(final BytesRef val) { String input = val.utf8ToString(); String decoded = URLDecoder.decode(input, StandardCharsets.UTF_8); 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 index 0a6196197233c..cbe90ae41792a 100644 --- 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 @@ -11,6 +11,8 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.compute.ann.ConvertEvaluator; +import org.elasticsearch.compute.ann.Fixed; +import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; @@ -22,12 +24,12 @@ 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 org.elasticsearch.xpack.esql.expression.function.scalar.util.UrlCodecUtils; import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.List; +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; public final class UrlEncode extends UnaryScalarFunction { @@ -45,11 +47,15 @@ private UrlEncode(StreamInput in) throws IOException { @FunctionInfo( returnType = "keyword", preview = true, - description = "URL encodes the input.", + description = "URL-encodes the input. All characters are percent-encoded except for alphanumerics, " + + "`.`, `-`, `_`, and `~`. Spaces are encoded as `+`.", 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) { + public UrlEncode( + Source source, + @Param(name = "string", type = { "keyword", "text" }, description = "The URL to encode.") Expression str + ) { super(source, str); } @@ -78,14 +84,16 @@ protected TypeResolution resolveType() { @Override public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { - return new UrlEncodeEvaluator.Factory(source(), toEvaluator.apply(field())); + return new UrlEncodeEvaluator.Factory( + source(), + toEvaluator.apply(field()), + context -> new BreakingBytesRefBuilder(context.breaker(), "url_encode") + ); } @ConvertEvaluator() - static BytesRef process(final BytesRef val) { - String input = val.utf8ToString(); - String encoded = URLEncoder.encode(input, StandardCharsets.UTF_8); - return new BytesRef(encoded); + static BytesRef process(final BytesRef val, @Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch) { + return UrlCodecUtils.urlEncode(val, scratch, true); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponent.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponent.java new file mode 100644 index 0000000000000..1b1c16aca1370 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponent.java @@ -0,0 +1,99 @@ +/* + * 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.ann.Fixed; +import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; +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 org.elasticsearch.xpack.esql.expression.function.scalar.util.UrlCodecUtils; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.compute.ann.Fixed.Scope.THREAD_LOCAL; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; + +public class UrlEncodeComponent extends UnaryScalarFunction { + + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Expression.class, + "UrlEncodeComponent", + UrlEncodeComponent::new + ); + + private UrlEncodeComponent(StreamInput in) throws IOException { + super(in); + } + + @FunctionInfo( + returnType = "keyword", + preview = true, + description = "URL-encodes the input. All characters are percent-encoded except for alphanumerics, " + + "`.`, `-`, `_`, and `~`. Spaces are encoded as `%20`.", + examples = { @Example(file = "string", tag = "url_encode_component") }, + appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.DEVELOPMENT) } + ) + public UrlEncodeComponent( + Source source, + @Param(name = "string", type = { "keyword", "text" }, description = "The URL to encode.") Expression str + ) { + super(source, str); + } + + @Override + public Expression replaceChildren(List newChildren) { + return new UrlEncodeComponent(source(), newChildren.get(0)); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, UrlEncodeComponent::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 UrlEncodeComponentEvaluator.Factory( + source(), + toEvaluator.apply(field()), + context -> new BreakingBytesRefBuilder(context.breaker(), "url_encode_component") + ); + } + + @ConvertEvaluator() + static BytesRef process(final BytesRef val, @Fixed(includeInToString = false, scope = THREAD_LOCAL) BreakingBytesRefBuilder scratch) { + return UrlCodecUtils.urlEncode(val, scratch, false); + } + +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/util/UrlCodecUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/util/UrlCodecUtils.java new file mode 100644 index 0000000000000..6395df1fd55b8 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/util/UrlCodecUtils.java @@ -0,0 +1,116 @@ +/* + * 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.util; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.operator.BreakingBytesRefBuilder; + +public final class UrlCodecUtils { + + private UrlCodecUtils() {} + + private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray(); + + public static BytesRef urlEncode(final BytesRef val, BreakingBytesRefBuilder scratch, final boolean plusForSpace) { + int size = computeSizeAfterEncoding(val, plusForSpace); + + if (size == -1) { + // the input doesn't change after encoding so encoding can be skipped + return val; + } + + scratch.grow(size); + scratch.clear(); + + int lo = val.offset; + int hi = val.offset + val.length; + + for (int i = lo; i < hi; ++i) { + byte b = val.bytes[i]; + char c = (char) (b & 0xFF); + + if (plusForSpace && c == ' ') { + scratch.append((byte) '+'); + continue; + } + + if (isRfc3986Safe(c)) { + scratch.append(b); + continue; + } + + // every encoded byte is represented by 3 chars: %XY + scratch.append((byte) '%'); + + // the X in %XY is the hex value for the high nibble + scratch.append((byte) HEX_DIGITS[(c >> 4) & 0x0F]); + + // the Y in %XY is the hex value for the low nibble + scratch.append((byte) HEX_DIGITS[c & 0x0F]); + } + + return scratch.bytesRefView(); + } + + /** + * Determines whether a character is considered unreserved (or safe) according to RFC3986. Alphanumerics along with ".-_~" are safe, + * and therefore not percent-encoded. + * + * @param c A character + * @return Boolean + */ + public static boolean isRfc3986Safe(char c) { + return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') || c == '-' || c == '.' || c == '_' || c == '~'; + } + + /** + *

Computes the size of the input if it were encoded, and tells whether any encoding is needed at all. For example, if the input only + * contained alphanumerics and safe characters, then -1 is returned, to mean that no encoding is needed. If the input additionally + * contained spaces which can be encoded as '+', then the new size after encoding is returned.

+ * + *

Examples

+ *
    + *
  • "abc" -> -1 (no encoding needed)
  • + *
  • "a b" -> 3 if encoding spaces as "+". The positive value indicates encoding is needed.
  • + *
  • "a b" -> 5 if encoding spaces as "%20". The positive value indicates encoding is needed.
  • + *
  • "" -> -1 (no encoding needed)
  • + *
+ * + * @param val + * @param plusForSpace Whether spaces are encoded as + or %20. + * @return The new size after encoding, or -1 if no encoding is needed. + */ + private static int computeSizeAfterEncoding(final BytesRef val, final boolean plusForSpace) { + int size = 0; + boolean noEncodingNeeded = true; + + int lo = val.offset; + int hi = val.offset + val.length; + + for (int i = lo; i < hi; ++i) { + char c = (char) (val.bytes[i] & 0xFF); + + if (plusForSpace && c == ' ') { + ++size; + noEncodingNeeded = false; + } else if (isRfc3986Safe(c)) { + ++size; + } else { + size += 3; + noEncodingNeeded = false; + } + } + + if (noEncodingNeeded) { + return -1; + } + + return size; + } + +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractUrlEncodeDecodeTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractUrlEncodeDecodeTestCase.java index 2c01868bb2cda..804bfa62c6c85 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractUrlEncodeDecodeTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractUrlEncodeDecodeTestCase.java @@ -7,99 +7,263 @@ package org.elasticsearch.xpack.esql.expression.function; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.net.PercentCodec; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.scalar.util.UrlCodecUtils; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.function.Supplier; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; public abstract class AbstractUrlEncodeDecodeTestCase extends AbstractScalarFunctionTestCase { - private record RandomUrl(String plain, String encoded) {} + private static final PercentCodec urlEncodeCodec; + private static final PercentCodec urlEncodeComponentCodec; + + public enum PercentCodecTestType { + ENCODE("UrlEncodeEvaluator[val=Attribute[channel=0]]"), + ENCODE_COMPONENT("UrlEncodeComponentEvaluator[val=Attribute[channel=0]]"), + DECODE("UrlDecodeEvaluator[val=Attribute[channel=0]]"); + + public final String evaluatorToString; + + PercentCodecTestType(String evaluatorToString) { + this.evaluatorToString = evaluatorToString; + } + + public PercentCodec getCodec() { + return switch (this) { + case ENCODE -> urlEncodeCodec; + case ENCODE_COMPONENT -> urlEncodeComponentCodec; - public static Iterable createParameters(boolean isEncoderTest) { - String evaluatorToString = isEncoderTest - ? "UrlEncodeEvaluator[val=Attribute[channel=0]]" - : "UrlDecodeEvaluator[val=Attribute[channel=0]]"; + // Randomized decoder tests apply a random encoder to the input to make it decodable. Fixed bad cases for the decoder skip + // this by design, in order to assert undecodable input is handled gracefully. + case DECODE -> randomBoolean() ? urlEncodeCodec : urlEncodeComponentCodec; + }; + } + } + static { + // Both codecs percent-encode all characters in the input except for alphanumerics, '-', '.', '_', and '~'. The space character is a + // special case, as it can be either percent-encoded or replaced with a '+'. + // During testing, the values generated by both encoders are considered as ground truth, so the results of our implementation + // must match that. + + // encodes spaces as '+' + byte[] b1 = buildUnsafeBytes(Set.of(' ')); + urlEncodeCodec = new PercentCodec(b1, true); + + // encodes spaces as '%20' + byte[] b2 = buildUnsafeBytes(Set.of()); + urlEncodeComponentCodec = new PercentCodec(b2, false); + } + + private record RandomUrl(String plain, String encoded) {} + + public static Iterable createParameters(PercentCodecTestType codecTestType) { List suppliers = new ArrayList<>(); for (DataType dataType : DataType.stringTypes()) { - Supplier caseSupplier = () -> createTestCaseWithRandomUrl( - dataType, - evaluatorToString, - isEncoderTest - ); - + // random URL tests + Supplier caseSupplier = () -> createTestCaseWithRandomUrl(dataType, codecTestType); suppliers.add(new TestCaseSupplier(List.of(dataType), caseSupplier)); + // random strings tests for (TestCaseSupplier.TypedDataSupplier supplier : TestCaseSupplier.stringCases(dataType)) { TestCaseSupplier testCaseSupplier = new TestCaseSupplier( supplier.name(), List.of(supplier.type()), - () -> createTestCaseWithRandomString(dataType, evaluatorToString, isEncoderTest, supplier) + () -> createTestCaseWithRandomString(dataType, codecTestType, supplier) ); suppliers.add(testCaseSupplier); } + + // fixed input tests + String[] fixedInputs = new String[] { + // all safe chars plus a space + "foo bar", + + // unicode: right-to-left override (U+202E), math symbols, etc. + "ab \u202E cd \u202E ef sigma:\u2211 delta:\u2206 tunes:\u266B radioactive:\u2622 hourglass:\u23F3", + + // safe and unsafe chars + "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + + // all ASCII chars + new String(allAsciiChars(), StandardCharsets.UTF_8) }; + + for (String input : fixedInputs) { + suppliers.add(createFixedTestCase(dataType, input, codecTestType)); + } + + if (codecTestType == PercentCodecTestType.DECODE) { + // bad inputs for decoder tests aren't encoded first (as they wouldn't be bad then), but are expected to be handled + // gracefully by the decoder. + + List> tuples = List.of( + // incomplete sequence + Tuple.tuple("%1", "Line 1:1: java.lang.IllegalArgumentException: URLDecoder: Incomplete trailing escape (%) pattern"), + + // missing sequence + Tuple.tuple("%", "Line 1:1: java.lang.IllegalArgumentException: URLDecoder: Incomplete trailing escape (%) pattern"), + + // invalid hex digits + Tuple.tuple( + "%xy", + "Line 1:1: java.lang.IllegalArgumentException: URLDecoder: Illegal hex characters in escape (%) pattern - " + + "not a hexadecimal digit: \"x\" = 120" + ), + + // valid and invalid sequences + Tuple.tuple( + "foo+bar%20qux%mn", + "Line 1:1: java.lang.IllegalArgumentException: URLDecoder: Illegal hex characters in escape (%) pattern - " + + "not a hexadecimal digit: \"m\" = 109" + ) + ); + + for (Tuple t : tuples) { + String undecodableInput = t.v1(); + String expectedErrorMessage = t.v2(); + suppliers.add(createBadDecoderTestCase(dataType, undecodableInput, expectedErrorMessage)); + } + } } return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(false, suppliers); } - public static TestCaseSupplier.TestCase createTestCaseWithRandomUrl( - DataType dataType, - String evaluatorToString, - boolean isEncoderTest - ) { - RandomUrl url = generateRandomUrl(); + public static TestCaseSupplier.TestCase createTestCaseWithRandomUrl(DataType dataType, PercentCodecTestType codecTestType) { + boolean isEncoderTest = (codecTestType != PercentCodecTestType.DECODE); + RandomUrl url = generateRandomUrl(codecTestType); BytesRef input = new BytesRef(isEncoderTest ? url.plain() : url.encoded()); BytesRef output = new BytesRef(isEncoderTest ? url.encoded() : url.plain()); TestCaseSupplier.TypedData fieldTypedData = new TestCaseSupplier.TypedData(input, dataType, "string"); - return new TestCaseSupplier.TestCase(List.of(fieldTypedData), evaluatorToString, dataType, equalTo(output)); + return new TestCaseSupplier.TestCase(List.of(fieldTypedData), codecTestType.evaluatorToString, dataType, equalTo(output)); } public static TestCaseSupplier.TestCase createTestCaseWithRandomString( DataType dataType, - String evaluatorToString, - boolean isEncoderTest, + PercentCodecTestType codecTestType, TestCaseSupplier.TypedDataSupplier supplier ) { + boolean isEncoderTest = (codecTestType != PercentCodecTestType.DECODE); TestCaseSupplier.TypedData fieldTypedData = supplier.get(); String plain = BytesRefs.toBytesRef(fieldTypedData.data()).utf8ToString(); - String encoded = encode(plain); + String encoded = encode(plain, codecTestType); BytesRef input = new BytesRef(isEncoderTest ? plain : encoded); BytesRef output = new BytesRef(isEncoderTest ? encoded : plain); return new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(input, dataType, "string")), - evaluatorToString, + codecTestType.evaluatorToString, dataType, equalTo(output) ); } - private static RandomUrl generateRandomUrl() { + private static RandomUrl generateRandomUrl(PercentCodecTestType codecTestType) { 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 space = " "; // ensure the correct encoding for space (+ or %20) - String plain = String.format(Locale.ROOT, "%s%s%s%s", protocol, domain, path, query); - String encoded = encode(plain); + String plain = String.format(Locale.ROOT, "%s%s%s%s%s", protocol, domain, path, query, space); + String encoded = encode(plain, codecTestType); return new RandomUrl(plain, encoded); } - private static String encode(String plain) { - return URLEncoder.encode(plain, StandardCharsets.UTF_8); + private static String encode(String plain, PercentCodecTestType codecTestType) { + byte[] plainBytes = plain.getBytes(StandardCharsets.UTF_8); + byte[] encoded = null; + + try { + encoded = codecTestType.getCodec().encode(plainBytes); + } catch (EncoderException ex) { + // Checked exception isn't really thrown, but we must handle it given the signature of PercentCodec.encode(). + throw new RuntimeException(ex); + } + + return new String(encoded, StandardCharsets.UTF_8); + } + + /** + * Builds the list of individual ASCII bytes that are considered unsafe; must always be percent-encoded. Bytes outside the + * ASCII range are always percent-encoded by the codecs are don't need to be included in our list. + * + * @param additionallySafe + * @return unsafe ASCII chars + */ + private static byte[] buildUnsafeBytes(final Set additionallySafe) { + Set unsafe = new HashSet<>(); + + for (int i = 0; i <= Byte.MAX_VALUE; ++i) { + char c = (char) i; + if (additionallySafe.contains(c) == false && UrlCodecUtils.isRfc3986Safe(c) == false) { + unsafe.add((byte) i); + } + } + + byte[] bytes = new byte[unsafe.size()]; + + int i = 0; + for (byte b : unsafe) { + bytes[i++] = b; + } + + return bytes; + } + + private static TestCaseSupplier createFixedTestCase(DataType dataType, String plain, PercentCodecTestType codecTestType) { + return new TestCaseSupplier(List.of(dataType), () -> { + boolean isEncoderTest = (codecTestType != PercentCodecTestType.DECODE); + String encoded = encode(plain, codecTestType); + String input = (isEncoderTest) ? plain : encoded; + String output = isEncoderTest ? encoded : plain; + + return new TestCaseSupplier.TestCase( + List.of(new TestCaseSupplier.TypedData(new BytesRef(input), dataType, "string")), + codecTestType.evaluatorToString, + dataType, + equalTo(new BytesRef(output)) + ); + }); + } + + private static TestCaseSupplier createBadDecoderTestCase(DataType dataType, String undecodable, String exceptionMessage) { + return new TestCaseSupplier( + List.of(dataType), + () -> new TestCaseSupplier.TestCase( + List.of(new TestCaseSupplier.TypedData(new BytesRef(undecodable), dataType, "string")), + PercentCodecTestType.DECODE.evaluatorToString, + dataType, + is(nullValue()) + ).withWarning("Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.") + .withWarning(exceptionMessage) + ); + } + + private static byte[] allAsciiChars() { + byte[] bytes = new byte[Byte.MAX_VALUE + 1]; + for (int i = 0; i < bytes.length; ++i) { + bytes[i] = (byte) i; + } + return bytes; } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeTests.java index 217ee206899d1..688383f9a0a30 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlDecodeTests.java @@ -26,7 +26,7 @@ public UrlDecodeTests(@Name("TestCase") Supplier test @ParametersFactory public static Iterable parameters() { - return createParameters(false); + return createParameters(PercentCodecTestType.DECODE); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentErrorTests.java new file mode 100644 index 0000000000000..283db30fa1c0d --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentErrorTests.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 UrlEncodeComponentErrorTests extends ErrorsForCasesWithoutExamplesTestCase { + @Override + protected List cases() { + return paramsToSuppliers(UrlEncodeComponentTests.parameters()); + } + + @Override + protected Expression build(Source source, List args) { + return new UrlEncodeComponent(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/UrlEncodeComponentSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentSerializationTests.java new file mode 100644 index 0000000000000..dca48f847aa47 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentSerializationTests.java @@ -0,0 +1,21 @@ +/* + * 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 UrlEncodeComponentSerializationTests extends AbstractUnaryScalarSerializationTests { + + @Override + protected UrlEncodeComponent create(Source source, Expression child) { + return new UrlEncodeComponent(source, child); + } + +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentTests.java new file mode 100644 index 0000000000000..04ee481c78ab6 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/UrlEncodeComponentTests.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 com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.function.AbstractUrlEncodeDecodeTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; + +import java.util.List; +import java.util.function.Supplier; + +public class UrlEncodeComponentTests extends AbstractUrlEncodeDecodeTestCase { + + public UrlEncodeComponentTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + return createParameters(PercentCodecTestType.ENCODE_COMPONENT); + } + + @Override + protected Expression build(Source source, List args) { + return new UrlEncodeComponent(source, args.get(0)); + } + +} 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 index ee57e71c30ba8..8791810d15c6c 100644 --- 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 @@ -26,7 +26,7 @@ public UrlEncodeTests(@Name("TestCase") Supplier test @ParametersFactory public static Iterable parameters() { - return createParameters(true); + return createParameters(PercentCodecTestType.ENCODE); } @Override