Browse Source

[ES|QL] Base64 decoding and encoding functions (#107390)

* add base64 functions
Fang Xing 1 year ago
parent
commit
353abef214
28 changed files with 830 additions and 1 deletions
  1. 5 0
      docs/reference/esql/functions/description/from_base64.asciidoc
  2. 5 0
      docs/reference/esql/functions/description/to_base64.asciidoc
  3. 13 0
      docs/reference/esql/functions/examples/from_base64.asciidoc
  4. 13 0
      docs/reference/esql/functions/examples/to_base64.asciidoc
  5. 35 0
      docs/reference/esql/functions/kibana/definition/from_base64.json
  6. 35 0
      docs/reference/esql/functions/kibana/definition/to_base64.json
  7. 11 0
      docs/reference/esql/functions/kibana/docs/from_base64.md
  8. 11 0
      docs/reference/esql/functions/kibana/docs/to_base64.md
  9. 15 0
      docs/reference/esql/functions/layout/from_base64.asciidoc
  10. 15 0
      docs/reference/esql/functions/layout/to_base64.asciidoc
  11. 6 0
      docs/reference/esql/functions/parameters/from_base64.asciidoc
  12. 6 0
      docs/reference/esql/functions/parameters/to_base64.asciidoc
  13. 1 0
      docs/reference/esql/functions/signature/from_base64.svg
  14. 1 0
      docs/reference/esql/functions/signature/to_base64.svg
  15. 4 0
      docs/reference/esql/functions/type-conversion-functions.asciidoc
  16. 10 0
      docs/reference/esql/functions/types/from_base64.asciidoc
  17. 10 0
      docs/reference/esql/functions/types/to_base64.asciidoc
  18. 9 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec
  19. 47 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec
  20. 120 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64Evaluator.java
  21. 131 0
      x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64Evaluator.java
  22. 4 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java
  23. 91 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64.java
  24. 90 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64.java
  25. 6 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java
  26. 6 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlFeatures.java
  27. 65 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64Tests.java
  28. 65 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64Tests.java

+ 5 - 0
docs/reference/esql/functions/description/from_base64.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Decode a base64 string.

+ 5 - 0
docs/reference/esql/functions/description/to_base64.asciidoc

@@ -0,0 +1,5 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Encode a string to a base64 string.

+ 13 - 0
docs/reference/esql/functions/examples/from_base64.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/string.csv-spec[tag=from_base64]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/string.csv-spec[tag=from_base64-result]
+|===
+

+ 13 - 0
docs/reference/esql/functions/examples/to_base64.asciidoc

@@ -0,0 +1,13 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Example*
+
+[source.merge.styled,esql]
+----
+include::{esql-specs}/string.csv-spec[tag=to_base64]
+----
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+include::{esql-specs}/string.csv-spec[tag=to_base64-result]
+|===
+

+ 35 - 0
docs/reference/esql/functions/kibana/definition/from_base64.json

@@ -0,0 +1,35 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "from_base64",
+  "description" : "Decode a base64 string.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "string",
+          "type" : "keyword",
+          "optional" : false,
+          "description" : "A base64 string."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "keyword"
+    },
+    {
+      "params" : [
+        {
+          "name" : "string",
+          "type" : "text",
+          "optional" : false,
+          "description" : "A base64 string."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "keyword"
+    }
+  ],
+  "examples" : [
+    "row a = \"ZWxhc3RpYw==\" \n| eval d = from_base64(a)"
+  ]
+}

+ 35 - 0
docs/reference/esql/functions/kibana/definition/to_base64.json

@@ -0,0 +1,35 @@
+{
+  "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.",
+  "type" : "eval",
+  "name" : "to_base64",
+  "description" : "Encode a string to a base64 string.",
+  "signatures" : [
+    {
+      "params" : [
+        {
+          "name" : "string",
+          "type" : "keyword",
+          "optional" : false,
+          "description" : "A string."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "keyword"
+    },
+    {
+      "params" : [
+        {
+          "name" : "string",
+          "type" : "text",
+          "optional" : false,
+          "description" : "A string."
+        }
+      ],
+      "variadic" : false,
+      "returnType" : "keyword"
+    }
+  ],
+  "examples" : [
+    "row a = \"elastic\" \n| eval e = to_base64(a)"
+  ]
+}

+ 11 - 0
docs/reference/esql/functions/kibana/docs/from_base64.md

@@ -0,0 +1,11 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### FROM_BASE64
+Decode a base64 string.
+
+```
+row a = "ZWxhc3RpYw==" 
+| eval d = from_base64(a)
+```

+ 11 - 0
docs/reference/esql/functions/kibana/docs/to_base64.md

@@ -0,0 +1,11 @@
+<!--
+This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+-->
+
+### TO_BASE64
+Encode a string to a base64 string.
+
+```
+row a = "elastic" 
+| eval e = to_base64(a)
+```

+ 15 - 0
docs/reference/esql/functions/layout/from_base64.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-from_base64]]
+=== `FROM_BASE64`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/from_base64.svg[Embedded,opts=inline]
+
+include::../parameters/from_base64.asciidoc[]
+include::../description/from_base64.asciidoc[]
+include::../types/from_base64.asciidoc[]
+include::../examples/from_base64.asciidoc[]

+ 15 - 0
docs/reference/esql/functions/layout/to_base64.asciidoc

@@ -0,0 +1,15 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-to_base64]]
+=== `TO_BASE64`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/to_base64.svg[Embedded,opts=inline]
+
+include::../parameters/to_base64.asciidoc[]
+include::../description/to_base64.asciidoc[]
+include::../types/to_base64.asciidoc[]
+include::../examples/to_base64.asciidoc[]

+ 6 - 0
docs/reference/esql/functions/parameters/from_base64.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`string`::
+A base64 string.

+ 6 - 0
docs/reference/esql/functions/parameters/to_base64.asciidoc

@@ -0,0 +1,6 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Parameters*
+
+`string`::
+A string.

+ 1 - 0
docs/reference/esql/functions/signature/from_base64.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="348" height="46" viewbox="0 0 348 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m152 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="152" height="36"/><text class="k" x="15" y="31">FROM_BASE64</text><rect class="s" x="167" y="5" width="32" height="36" rx="7"/><text class="syn" x="177" y="31">(</text><rect class="s" x="209" y="5" width="92" height="36" rx="7"/><text class="k" x="219" y="31">string</text><rect class="s" x="311" y="5" width="32" height="36" rx="7"/><text class="syn" x="321" y="31">)</text></svg>

+ 1 - 0
docs/reference/esql/functions/signature/to_base64.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="324" height="46" viewbox="0 0 324 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m128 0h10m32 0h10m92 0h10m32 0h5"/><rect class="s" x="5" y="5" width="128" height="36"/><text class="k" x="15" y="31">TO_BASE64</text><rect class="s" x="143" y="5" width="32" height="36" rx="7"/><text class="syn" x="153" y="31">(</text><rect class="s" x="185" y="5" width="92" height="36" rx="7"/><text class="k" x="195" y="31">string</text><rect class="s" x="287" y="5" width="32" height="36" rx="7"/><text class="syn" x="297" y="31">)</text></svg>

+ 4 - 0
docs/reference/esql/functions/type-conversion-functions.asciidoc

@@ -8,6 +8,8 @@
 {esql} supports these type conversion functions:
 
 // tag::type_list[]
+* <<esql-from_base64>>
+* <<esql-to_base64>>
 * <<esql-to_boolean>>
 * <<esql-to_cartesianpoint>>
 * <<esql-to_cartesianshape>>
@@ -25,6 +27,8 @@
 * <<esql-to_version>>
 // end::type_list[]
 
+include::layout/from_base64.asciidoc[]
+include::layout/to_base64.asciidoc[]
 include::to_boolean.asciidoc[]
 include::to_cartesianpoint.asciidoc[]
 include::to_cartesianshape.asciidoc[]

+ 10 - 0
docs/reference/esql/functions/types/from_base64.asciidoc

@@ -0,0 +1,10 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+string | result
+keyword | keyword
+text | keyword
+|===

+ 10 - 0
docs/reference/esql/functions/types/to_base64.asciidoc

@@ -0,0 +1,10 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+string | result
+keyword | keyword
+text | keyword
+|===

+ 9 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec

@@ -26,6 +26,7 @@ synopsis:keyword
 double e()
 "boolean ends_with(str:keyword|text, suffix:keyword|text)"
 "double|integer|long|unsigned_long floor(number:double|integer|long|unsigned_long)"
+"keyword from_base64(string:keyword|text)"
 "integer|long|double|boolean|keyword|text|ip|version greatest(first:integer|long|double|boolean|keyword|text|ip|version, ?rest...:integer|long|double|boolean|keyword|text|ip|version)"
 "integer|long|double|boolean|keyword|text|ip|version least(first:integer|long|double|boolean|keyword|text|ip|version, ?rest...:integer|long|double|boolean|keyword|text|ip|version)"
 "keyword left(string:keyword|text, length:integer)"
@@ -77,6 +78,7 @@ double pi()
 "double tan(angle:double|integer|long|unsigned_long)"
 "double tanh(angle:double|integer|long|unsigned_long)"
 double tau()
+"keyword to_base64(string:keyword|text)"
 "boolean to_bool(field:boolean|keyword|text|double|long|unsigned_long|integer)"
 "boolean to_boolean(field:boolean|keyword|text|double|long|unsigned_long|integer)"
 "cartesian_point to_cartesianpoint(field:cartesian_point|keyword|text)"
@@ -136,6 +138,7 @@ date_trunc    |[interval, date]                    |["date_period|time_duration"
 e             |null                                |null                                                                                                                              |null
 ends_with     |[str, suffix]                       |["keyword|text", "keyword|text"]                                                                                                  |[, ]
 floor         |number                              |"double|integer|long|unsigned_long"                                                                                               |Numeric expression. If `null`, the function returns `null`.
+from_base64   |string                              |"keyword|text"                                                                                                                    |A base64 string.
 greatest      |first                               |"integer|long|double|boolean|keyword|text|ip|version"                                                                             |[""]
 least         |first                               |"integer|long|double|boolean|keyword|text|ip|version"                                                                             |[""]
 left          |[string, length]                    |["keyword|text", integer]                                                                                                         |[The string from which to return a substring., The number of characters to return.]
@@ -187,6 +190,7 @@ sum           |number                              |"double|integer|long"
 tan           |angle                               |"double|integer|long|unsigned_long"                                                                                               |An angle, in radians. If `null`, the function returns `null`.
 tanh          |angle                               |"double|integer|long|unsigned_long"                                                                                               |An angle, in radians. If `null`, the function returns `null`.
 tau           |null                                |null                                                                                                                              |null
+to_base64     |string                              |"keyword|text"                                                                                                                    |A string.
 to_bool       |field                               |"boolean|keyword|text|double|long|unsigned_long|integer"                                                                          |[""]
 to_boolean    |field                               |"boolean|keyword|text|double|long|unsigned_long|integer"                                                                          |[""]
 to_cartesianpo|field                               |"cartesian_point|keyword|text"                                                                                                    |[""]
@@ -247,6 +251,7 @@ date_trunc    |Rounds down a date to the closest interval.
 e             |Returns {wikipedia}/E_(mathematical_constant)[Euler's number].
 ends_with     |Returns a boolean that indicates whether a keyword string ends with another string
 floor         |Round a number down to the nearest integer.
+from_base64   |Decode a base64 string.
 greatest      |Returns the maximum value from many columns.
 least         |Returns the minimum value from many columns.
 left          |Returns the substring that extracts 'length' chars from 'string' starting from the left.
@@ -298,6 +303,7 @@ sum           |The sum of a numeric field.
 tan           |Returns the {wikipedia}/Sine_and_cosine[Tangent] trigonometric function of an angle.
 tanh          |Returns the {wikipedia}/Hyperbolic_functions[Tangent] hyperbolic function of an angle.
 tau           |The ratio of a circle’s circumference to its radius.
+to_base64     |Encode a string to a base64 string.
 to_bool       |Converts an input value to a boolean value.
 to_boolean    |Converts an input value to a boolean value.
 to_cartesianpo|Converts an input value to a point value.
@@ -359,6 +365,7 @@ date_trunc    |date
 e             |double                                                                                                                      |null                        |false           |false
 ends_with     |boolean                                                                                                                     |[false, false]              |false           |false
 floor         |"double|integer|long|unsigned_long"                                                                                         |false                       |false           |false
+from_base64   |keyword                                                                                                                     |false                       |false           |false
 greatest      |"integer|long|double|boolean|keyword|text|ip|version"                                                                       |false                       |true            |false
 least         |"integer|long|double|boolean|keyword|text|ip|version"                                                                       |false                       |true            |false
 left          |keyword                                                                                                                     |[false, false]              |false           |false
@@ -410,6 +417,7 @@ sum           |long
 tan           |double                                                                                                                      |false                       |false           |false
 tanh          |double                                                                                                                      |false                       |false           |false
 tau           |double                                                                                                                      |null                        |false           |false
+to_base64     |keyword                                                                                                                     |false                       |false           |false
 to_bool       |boolean                                                                                                                     |false                       |false           |false
 to_boolean    |boolean                                                                                                                     |false                       |false           |false
 to_cartesianpo|cartesian_point                                                                                                             |false                       |false           |false
@@ -455,5 +463,5 @@ countFunctions#[skip:-8.13.99]
 meta functions |  stats  a = count(*), b = count(*), c = count(*) |  mv_expand c;
 
 a:long | b:long | c:long
-102    | 102    | 102
+104    | 104    | 104
 ;

+ 47 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec

@@ -1320,3 +1320,50 @@ l1:integer | l2:integer
 2          | null
 null       | 0
 ;
+
+base64Encode#[skip:-8.13.99,reason:new base64 function added in 8.14]
+required_feature: esql.base64_decode_encode
+
+// tag::to_base64[]
+row a = "elastic" 
+| eval e = to_base64(a)
+// end::to_base64[]
+;
+
+// tag::to_base64-result[]
+a:keyword | e:keyword
+elastic   | ZWxhc3RpYw==
+// end::to_base64-result[]
+;
+
+base64Decode#[skip:-8.13.99,reason:new base64 function added in 8.14]
+required_feature: esql.base64_decode_encode
+
+// tag::from_base64[]
+row a = "ZWxhc3RpYw==" 
+| eval d = from_base64(a)
+// end::from_base64[]
+;
+
+// tag::from_base64-result[]
+a:keyword    | d:keyword
+ZWxhc3RpYw== | elastic
+// end::from_base64-result[]
+;
+
+base64EncodeDecodeEmp#[skip:-8.13.99,reason:new base64 function added in 8.14]
+required_feature: esql.base64_decode_encode
+
+from employees
+| where emp_no < 10032 and emp_no > 10027
+| eval e = to_base64(first_name), d = from_base64(e)
+| keep emp_no, first_name, e, d
+| sort emp_no
+;
+
+emp_no:integer | first_name:keyword | e:keyword    | d:keyword
+10028          | Domenick           | RG9tZW5pY2s= | Domenick
+10029          | Otmar              | T3RtYXI=     | Otmar
+10030          | null               | null         | null
+10031          | null               | null         | null
+;

+ 120 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64Evaluator.java

@@ -0,0 +1,120 @@
+// 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.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.util.function.Function;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link FromBase64}.
+ * This class is generated. Do not edit it.
+ */
+public final class FromBase64Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final BytesRefBuilder oScratch;
+
+  private final DriverContext driverContext;
+
+  public FromBase64Evaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      BytesRefBuilder oScratch, DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.field = field;
+    this.oScratch = oScratch;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock fieldBlock = (BytesRefBlock) field.eval(page)) {
+      BytesRefVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector).asBlock();
+    }
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefBlock fieldBlock) {
+    try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      BytesRef fieldScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        result.appendBytesRef(FromBase64.process(fieldBlock.getBytesRef(fieldBlock.getFirstValueIndex(p), fieldScratch), oScratch));
+      }
+      return result.build();
+    }
+  }
+
+  public BytesRefVector eval(int positionCount, BytesRefVector fieldVector) {
+    try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) {
+      BytesRef fieldScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        result.appendBytesRef(FromBase64.process(fieldVector.getBytesRef(p, fieldScratch), oScratch));
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "FromBase64Evaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    private final Function<DriverContext, BytesRefBuilder> oScratch;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field,
+        Function<DriverContext, BytesRefBuilder> oScratch) {
+      this.source = source;
+      this.field = field;
+      this.oScratch = oScratch;
+    }
+
+    @Override
+    public FromBase64Evaluator get(DriverContext context) {
+      return new FromBase64Evaluator(source, field.get(context), oScratch.apply(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "FromBase64Evaluator[" + "field=" + field + "]";
+    }
+  }
+}

+ 131 - 0
x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64Evaluator.java

@@ -0,0 +1,131 @@
+// 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.ArithmeticException;
+import java.lang.IllegalArgumentException;
+import java.lang.Override;
+import java.lang.String;
+import java.util.function.Function;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.elasticsearch.compute.data.Block;
+import org.elasticsearch.compute.data.BytesRefBlock;
+import org.elasticsearch.compute.data.BytesRefVector;
+import org.elasticsearch.compute.data.Page;
+import org.elasticsearch.compute.operator.DriverContext;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.core.Releasables;
+import org.elasticsearch.xpack.esql.expression.function.Warnings;
+import org.elasticsearch.xpack.ql.tree.Source;
+
+/**
+ * {@link EvalOperator.ExpressionEvaluator} implementation for {@link ToBase64}.
+ * This class is generated. Do not edit it.
+ */
+public final class ToBase64Evaluator implements EvalOperator.ExpressionEvaluator {
+  private final Warnings warnings;
+
+  private final EvalOperator.ExpressionEvaluator field;
+
+  private final BytesRefBuilder oScratch;
+
+  private final DriverContext driverContext;
+
+  public ToBase64Evaluator(Source source, EvalOperator.ExpressionEvaluator field,
+      BytesRefBuilder oScratch, DriverContext driverContext) {
+    this.warnings = new Warnings(source);
+    this.field = field;
+    this.oScratch = oScratch;
+    this.driverContext = driverContext;
+  }
+
+  @Override
+  public Block eval(Page page) {
+    try (BytesRefBlock fieldBlock = (BytesRefBlock) field.eval(page)) {
+      BytesRefVector fieldVector = fieldBlock.asVector();
+      if (fieldVector == null) {
+        return eval(page.getPositionCount(), fieldBlock);
+      }
+      return eval(page.getPositionCount(), fieldVector);
+    }
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefBlock fieldBlock) {
+    try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      BytesRef fieldScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        if (fieldBlock.isNull(p)) {
+          result.appendNull();
+          continue position;
+        }
+        if (fieldBlock.getValueCount(p) != 1) {
+          if (fieldBlock.getValueCount(p) > 1) {
+            warnings.registerException(new IllegalArgumentException("single-value function encountered multi-value"));
+          }
+          result.appendNull();
+          continue position;
+        }
+        try {
+          result.appendBytesRef(ToBase64.process(fieldBlock.getBytesRef(fieldBlock.getFirstValueIndex(p), fieldScratch), oScratch));
+        } catch (ArithmeticException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  public BytesRefBlock eval(int positionCount, BytesRefVector fieldVector) {
+    try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) {
+      BytesRef fieldScratch = new BytesRef();
+      position: for (int p = 0; p < positionCount; p++) {
+        try {
+          result.appendBytesRef(ToBase64.process(fieldVector.getBytesRef(p, fieldScratch), oScratch));
+        } catch (ArithmeticException e) {
+          warnings.registerException(e);
+          result.appendNull();
+        }
+      }
+      return result.build();
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "ToBase64Evaluator[" + "field=" + field + "]";
+  }
+
+  @Override
+  public void close() {
+    Releasables.closeExpectNoException(field);
+  }
+
+  static class Factory implements EvalOperator.ExpressionEvaluator.Factory {
+    private final Source source;
+
+    private final EvalOperator.ExpressionEvaluator.Factory field;
+
+    private final Function<DriverContext, BytesRefBuilder> oScratch;
+
+    public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory field,
+        Function<DriverContext, BytesRefBuilder> oScratch) {
+      this.source = source;
+      this.field = field;
+      this.oScratch = oScratch;
+    }
+
+    @Override
+    public ToBase64Evaluator get(DriverContext context) {
+      return new ToBase64Evaluator(source, field.get(context), oScratch.apply(context), context);
+    }
+
+    @Override
+    public String toString() {
+      return "ToBase64Evaluator[" + "field=" + field + "]";
+    }
+  }
+}

+ 4 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java

@@ -21,6 +21,8 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Values;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Least;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.FromBase64;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToBase64;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToBoolean;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToCartesianPoint;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToCartesianShape;
@@ -254,6 +256,8 @@ public final class EsqlFunctionRegistry extends FunctionRegistry {
             new FunctionDefinition[] { def(CIDRMatch.class, CIDRMatch::new, "cidr_match") },
             // conversion functions
             new FunctionDefinition[] {
+                def(FromBase64.class, FromBase64::new, "from_base64"),
+                def(ToBase64.class, ToBase64::new, "to_base64"),
                 def(ToBoolean.class, ToBoolean::new, "to_boolean", "to_bool"),
                 def(ToCartesianPoint.class, ToCartesianPoint::new, "to_cartesianpoint"),
                 def(ToCartesianShape.class, ToCartesianShape::new, "to_cartesianshape"),

+ 91 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64.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.apache.lucene.util.BytesRefBuilder;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+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.planner.PlannerUtils;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.Base64;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
+import static org.elasticsearch.xpack.ql.type.DataTypes.KEYWORD;
+
+public class FromBase64 extends UnaryScalarFunction {
+
+    @FunctionInfo(
+        returnType = "keyword",
+        description = "Decode a base64 string.",
+        examples = @Example(file = "string", tag = "from_base64")
+    )
+    public FromBase64(
+        Source source,
+        @Param(name = "string", type = { "keyword", "text" }, description = "A base64 string.") Expression string
+    ) {
+        super(source, string);
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+        return isString(field, sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public DataType dataType() {
+        return KEYWORD;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new FromBase64(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, FromBase64::new, field());
+    }
+
+    @Evaluator()
+    static BytesRef process(BytesRef field, @Fixed(includeInToString = false, build = true) BytesRefBuilder oScratch) {
+        byte[] bytes = new byte[field.length];
+        System.arraycopy(field.bytes, field.offset, bytes, 0, field.length);
+        oScratch.grow(field.length);
+        oScratch.clear();
+        int decodedSize = Base64.getDecoder().decode(bytes, oScratch.bytes());
+        return new BytesRef(oScratch.bytes(), 0, decodedSize);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(
+        Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+    ) {
+        return switch (PlannerUtils.toElementType(field.dataType())) {
+            case BYTES_REF -> new FromBase64Evaluator.Factory(source(), toEvaluator.apply(field), context -> new BytesRefBuilder());
+            case NULL -> EvalOperator.CONSTANT_NULL_FACTORY;
+            default -> throw EsqlIllegalArgumentException.illegalDataType(field.dataType());
+        };
+    }
+}

+ 90 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64.java

@@ -0,0 +1,90 @@
+/*
+ * 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.apache.lucene.util.BytesRefBuilder;
+import org.elasticsearch.compute.ann.Evaluator;
+import org.elasticsearch.compute.ann.Fixed;
+import org.elasticsearch.compute.operator.EvalOperator;
+import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
+import org.elasticsearch.xpack.esql.expression.function.Example;
+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.planner.PlannerUtils;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.expression.TypeResolutions;
+import org.elasticsearch.xpack.ql.tree.NodeInfo;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+
+import java.util.Base64;
+import java.util.List;
+import java.util.function.Function;
+
+import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
+import static org.elasticsearch.xpack.ql.type.DataTypes.KEYWORD;
+
+public class ToBase64 extends UnaryScalarFunction {
+
+    @FunctionInfo(
+        returnType = "keyword",
+        description = "Encode a string to a base64 string.",
+        examples = @Example(file = "string", tag = "to_base64")
+    )
+    public ToBase64(Source source, @Param(name = "string", type = { "keyword", "text" }, description = "A string.") Expression string) {
+        super(source, string);
+
+    }
+
+    @Override
+    protected TypeResolution resolveType() {
+        if (childrenResolved() == false) {
+            return new TypeResolution("Unresolved children");
+        }
+        return isString(field, sourceText(), TypeResolutions.ParamOrdinal.DEFAULT);
+    }
+
+    @Override
+    public DataType dataType() {
+        return KEYWORD;
+    }
+
+    @Override
+    public Expression replaceChildren(List<Expression> newChildren) {
+        return new ToBase64(source(), newChildren.get(0));
+    }
+
+    @Override
+    protected NodeInfo<? extends Expression> info() {
+        return NodeInfo.create(this, ToBase64::new, field);
+    }
+
+    @Evaluator(warnExceptions = { ArithmeticException.class })
+    static BytesRef process(BytesRef field, @Fixed(includeInToString = false, build = true) BytesRefBuilder oScratch) {
+        int outLength = Math.multiplyExact(4, (Math.addExact(field.length, 2) / 3));
+        byte[] bytes = new byte[field.length];
+        System.arraycopy(field.bytes, field.offset, bytes, 0, field.length);
+        oScratch.grow(outLength);
+        oScratch.clear();
+        int encodedSize = Base64.getEncoder().encode(bytes, oScratch.bytes());
+        return new BytesRef(oScratch.bytes(), 0, encodedSize);
+    }
+
+    @Override
+    public EvalOperator.ExpressionEvaluator.Factory toEvaluator(
+        Function<Expression, EvalOperator.ExpressionEvaluator.Factory> toEvaluator
+    ) {
+        return switch (PlannerUtils.toElementType(field.dataType())) {
+            case BYTES_REF -> new ToBase64Evaluator.Factory(source(), toEvaluator.apply(field), context -> new BytesRefBuilder());
+            case NULL -> EvalOperator.CONSTANT_NULL_FACTORY;
+            default -> throw EsqlIllegalArgumentException.illegalDataType(field.dataType());
+        };
+    }
+}

+ 6 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/io/stream/PlanNamedTypes.java

@@ -42,6 +42,8 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.UnaryScalarFuncti
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest;
 import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Least;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.FromBase64;
+import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToBase64;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToBoolean;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToCartesianPoint;
 import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToCartesianShape;
@@ -348,6 +350,7 @@ public final class PlanNamedTypes {
             of(ESQL_UNARY_SCLR_CLS, Cos.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Cosh.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Floor.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
+            of(ESQL_UNARY_SCLR_CLS, FromBase64.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Length.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Log10.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, LTrim.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
@@ -360,6 +363,7 @@ public final class PlanNamedTypes {
             of(ESQL_UNARY_SCLR_CLS, StY.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Tan.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, Tanh.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
+            of(ESQL_UNARY_SCLR_CLS, ToBase64.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, ToBoolean.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, ToCartesianPoint.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
             of(ESQL_UNARY_SCLR_CLS, ToDatetime.class, PlanNamedTypes::writeESQLUnaryScalar, PlanNamedTypes::readESQLUnaryScalar),
@@ -1297,6 +1301,7 @@ public final class PlanNamedTypes {
         entry(name(Cos.class), Cos::new),
         entry(name(Cosh.class), Cosh::new),
         entry(name(Floor.class), Floor::new),
+        entry(name(FromBase64.class), FromBase64::new),
         entry(name(Length.class), Length::new),
         entry(name(Log10.class), Log10::new),
         entry(name(LTrim.class), LTrim::new),
@@ -1310,6 +1315,7 @@ public final class PlanNamedTypes {
         entry(name(StY.class), StY::new),
         entry(name(Tan.class), Tan::new),
         entry(name(Tanh.class), Tanh::new),
+        entry(name(ToBase64.class), ToBase64::new),
         entry(name(ToBoolean.class), ToBoolean::new),
         entry(name(ToCartesianPoint.class), ToCartesianPoint::new),
         entry(name(ToDatetime.class), ToDatetime::new),

+ 6 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlFeatures.java

@@ -105,11 +105,17 @@ public class EsqlFeatures implements FeatureSpecification {
      */
     public static final NodeFeature STRING_LITERAL_AUTO_CASTING = new NodeFeature("esql.string_literal_auto_casting");
 
+    /**
+     * Base64 encoding and decoding functions.
+     */
+    public static final NodeFeature BASE64_DECODE_ENCODE = new NodeFeature("esql.base64_decode_encode");
+
     @Override
     public Set<NodeFeature> getFeatures() {
         return Set.of(
             ASYNC_QUERY,
             AGG_VALUES,
+            BASE64_DECODE_ENCODE,
             MV_SORT,
             DISABLE_NULLABLE_OPTS,
             ST_X_Y,

+ 65 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/FromBase64Tests.java

@@ -0,0 +1,65 @@
+/*
+ * 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.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+@FunctionName("from_base64")
+public class FromBase64Tests extends AbstractFunctionTestCase {
+    public FromBase64Tests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+        suppliers.add(new TestCaseSupplier(List.of(DataTypes.KEYWORD), () -> {
+            BytesRef input = new BytesRef(randomAlphaOfLength(6));
+            return new TestCaseSupplier.TestCase(
+                List.of(new TestCaseSupplier.TypedData(input, DataTypes.KEYWORD, "string")),
+                "FromBase64Evaluator[field=Attribute[channel=0]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(Base64.getDecoder().decode(input.utf8ToString().getBytes(StandardCharsets.UTF_8))))
+            );
+        }));
+
+        suppliers.add(new TestCaseSupplier(List.of(DataTypes.TEXT), () -> {
+            BytesRef input = new BytesRef(randomAlphaOfLength(54));
+            return new TestCaseSupplier.TestCase(
+                List.of(new TestCaseSupplier.TypedData(input, DataTypes.TEXT, "string")),
+                "FromBase64Evaluator[field=Attribute[channel=0]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(Base64.getDecoder().decode(input.utf8ToString().getBytes(StandardCharsets.UTF_8))))
+            );
+        }));
+
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers)));
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new FromBase64(source, args.get(0));
+    }
+}

+ 65 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToBase64Tests.java

@@ -0,0 +1,65 @@
+/*
+ * 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.xpack.esql.expression.function.AbstractFunctionTestCase;
+import org.elasticsearch.xpack.esql.expression.function.FunctionName;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+@FunctionName("to_base64")
+public class ToBase64Tests extends AbstractFunctionTestCase {
+    public ToBase64Tests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        List<TestCaseSupplier> suppliers = new ArrayList<>();
+        suppliers.add(new TestCaseSupplier(List.of(DataTypes.KEYWORD), () -> {
+            BytesRef input = (BytesRef) randomLiteral(DataTypes.KEYWORD).value();
+            return new TestCaseSupplier.TestCase(
+                List.of(new TestCaseSupplier.TypedData(input, DataTypes.KEYWORD, "string")),
+                "ToBase64Evaluator[field=Attribute[channel=0]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(Base64.getEncoder().encode(input.utf8ToString().getBytes(StandardCharsets.UTF_8))))
+            );
+        }));
+
+        suppliers.add(new TestCaseSupplier(List.of(DataTypes.TEXT), () -> {
+            BytesRef input = (BytesRef) randomLiteral(DataTypes.TEXT).value();
+            return new TestCaseSupplier.TestCase(
+                List.of(new TestCaseSupplier.TypedData(input, DataTypes.TEXT, "string")),
+                "ToBase64Evaluator[field=Attribute[channel=0]]",
+                DataTypes.KEYWORD,
+                equalTo(new BytesRef(Base64.getEncoder().encode(input.utf8ToString().getBytes(StandardCharsets.UTF_8))))
+            );
+        }));
+
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers)));
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new ToBase64(source, args.get(0));
+    }
+}