This is an automated email from the ASF dual-hosted git repository.

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 3f88dc9719 [Feature][Transform-V2] Add Base64 SQL functions (#11114)
3f88dc9719 is described below

commit 3f88dc9719622c669ed05a3719c02775628addb4
Author: Kang Myeong Gwan <[email protected]>
AuthorDate: Sat Jun 20 07:06:10 2026 +0900

    [Feature][Transform-V2] Add Base64 SQL functions (#11114)
---
 docs/en/transforms/sql-functions.md                |  37 ++++++-
 docs/zh/transforms/sql-functions.md                |  38 ++++++-
 .../test/resources/sql_transform/func_string.conf  |  32 +++++-
 .../transform/sql/zeta/ZetaSQLFunction.java        |   6 ++
 .../seatunnel/transform/sql/zeta/ZetaSQLType.java  |   2 +
 .../sql/zeta/functions/StringFunction.java         |  43 ++++++++
 .../transform/sql/SQLStringFunctionsTest.java      |  75 ++++++++++++++
 .../sql/zeta/functions/StringFunctionTest.java     | 111 +++++++++++++++++++++
 8 files changed, 340 insertions(+), 4 deletions(-)

diff --git a/docs/en/transforms/sql-functions.md 
b/docs/en/transforms/sql-functions.md
index 2c6bd6bc17..28b8372067 100644
--- a/docs/en/transforms/sql-functions.md
+++ b/docs/en/transforms/sql-functions.md
@@ -94,6 +94,42 @@ Example:
 
 RAWTOHEX(DATA)
 
+### TO_BASE64
+
+```TO_BASE64(value[, charset]) -> STRING```
+
+Encodes a string or bytes to Base64.
+
+The default charset is `UTF-8`. You can specify another charset.
+
+For bytes input, the charset argument is not supported because the value is 
already raw bytes.
+
+Returns **NULL** if value is **NULL**.
+
+Example:
+
+TO_BASE64(NAME)
+
+TO_BASE64(NAME, 'UTF-16')
+
+TO_BASE64(BINARY_PAYLOAD)
+
+### FROM_BASE64
+
+```FROM_BASE64(value[, charset]) -> STRING```
+
+Decodes a Base64 string to text.
+
+The default charset is `UTF-8`. You can specify another charset.
+
+Returns **NULL** if value is **NULL**.
+
+Example:
+
+FROM_BASE64(ENCODED_NAME)
+
+FROM_BASE64(TO_BASE64(NAME, 'UTF-16'), 'UTF-16')
+
 ### INSERT
 
 ```INSERT(originalString, startInt, lengthInt, addString) -> STRING```
@@ -1350,4 +1386,3 @@ Normalizes a vector to unit length (magnitude = 1). This 
is useful for computing
 ```sql
 SELECT id, VECTOR_NORMALIZE(embedding) as normalized_embedding FROM table
 ```
-
diff --git a/docs/zh/transforms/sql-functions.md 
b/docs/zh/transforms/sql-functions.md
index 5a2536fa42..9349ee1291 100644
--- a/docs/zh/transforms/sql-functions.md
+++ b/docs/zh/transforms/sql-functions.md
@@ -94,6 +94,42 @@ HEXTORAW(DATA)
 
 RAWTOHEX(DATA)
 
+### TO_BASE64
+
+```TO_BASE64(value[, charset]) -> STRING```
+
+将字符串或字节编码为 Base64。
+
+默认字符集为 `UTF-8`。可以指定其他字符集。
+
+对于字节输入,不支持 charset 参数,因为该值已经是原始字节。
+
+如果 value 为 **NULL**,返回 **NULL**。
+
+示例:
+
+TO_BASE64(NAME)
+
+TO_BASE64(NAME, 'UTF-16')
+
+TO_BASE64(BINARY_PAYLOAD)
+
+### FROM_BASE64
+
+```FROM_BASE64(value[, charset]) -> STRING```
+
+将 Base64 字符串解码为文本。
+
+默认字符集为 `UTF-8`。可以指定其他字符集。
+
+如果 value 为 **NULL**,返回 **NULL**。
+
+示例:
+
+FROM_BASE64(ENCODED_NAME)
+
+FROM_BASE64(TO_BASE64(NAME, 'UTF-16'), 'UTF-16')
+
 ### INSERT
 
 ```INSERT(originalString, startInt, lengthInt, addString) -> STRING```
@@ -1353,4 +1389,4 @@ SELECT id, VECTOR_REDUCE(embedding, 64, 
'SPARSE_RANDOM_PROJECTION') as reduced_e
 **示例:**
 ```sql
 SELECT id, VECTOR_NORMALIZE(embedding) as normalized_embedding FROM table
-```
\ No newline at end of file
+```
diff --git 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf
 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf
index 3f0ad98b0b..192b6229e4 100644
--- 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf
+++ 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_string.conf
@@ -54,7 +54,7 @@ transform {
   sql {
     plugin_input = "fake"
     plugin_output = "fake1"
-    query = "select ascii(c1) as c1_1, ascii(c2) as c2_1, bit_length(c4) as 
c4_1, length(c4) as c4_2, octet_length(c4) as c4_3, char(c5) as c5_1, 
concat(c1,id,'!') as c1_2, hextoraw(c6) as c6_1, rawtohex(c7) as c7_1, 
insert(name,2,2,'**') as name1, lower(name) as name2, upper(name) as name3, 
left(name, 3) as name4, right(name, 4) as name5, lpad(name, 10, '*') as name6, 
rpad(name, 10, '*') as name7, ltrim(c8, '*') as c8_1, rtrim(c8, '*') as c8_2, 
trim(c8, '*') as c8_3, regexp_replace(c9,  [...]
+    query = "select ascii(c1) as c1_1, ascii(c2) as c2_1, bit_length(c4) as 
c4_1, length(c4) as c4_2, octet_length(c4) as c4_3, char(c5) as c5_1, 
concat(c1,id,'!') as c1_2, hextoraw(c6) as c6_1, rawtohex(c7) as c7_1, 
insert(name,2,2,'**') as name1, lower(name) as name2, upper(name) as name3, 
left(name, 3) as name4, right(name, 4) as name5, lpad(name, 10, '*') as name6, 
rpad(name, 10, '*') as name7, ltrim(c8, '*') as c8_1, rtrim(c8, '*') as c8_2, 
trim(c8, '*') as c8_3, regexp_replace(c9,  [...]
   }
 }
 
@@ -295,8 +295,36 @@ sink {
               rule_type = NOT_NULL
             }
           ]
+        },
+        {
+          field_name = "name16"
+          field_type = "string"
+          field_value = [
+            {equals_to = "Sm95IERpbmc="}
+          ]
+        },
+        {
+          field_name = "name17"
+          field_type = "string"
+          field_value = [
+            {equals_to = "Joy Ding"}
+          ]
+        },
+        {
+          field_name = "name18"
+          field_type = "string"
+          field_value = [
+            {equals_to = "/v8ASgBvAHkAIABEAGkAbgBn"}
+          ]
+        },
+        {
+          field_name = "name19"
+          field_type = "string"
+          field_value = [
+            {equals_to = "Joy Ding"}
+          ]
         }
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
index e339434614..d6a21edc1e 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
@@ -96,6 +96,8 @@ public class ZetaSQLFunction {
     public static final String CONCAT_WS = "CONCAT_WS";
     public static final String HEXTORAW = "HEXTORAW";
     public static final String RAWTOHEX = "RAWTOHEX";
+    public static final String TO_BASE64 = "TO_BASE64";
+    public static final String FROM_BASE64 = "FROM_BASE64";
     public static final String INSERT = "INSERT";
     public static final String LOWER = "LOWER";
     public static final String LCASE = "LCASE";
@@ -475,6 +477,10 @@ public class ZetaSQLFunction {
                 return StringFunction.hextoraw(args);
             case RAWTOHEX:
                 return StringFunction.rawtohex(args);
+            case TO_BASE64:
+                return StringFunction.toBase64(args);
+            case FROM_BASE64:
+                return StringFunction.fromBase64(args);
             case INSERT:
                 return StringFunction.insert(args);
             case LOWER:
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
index 5988f0869d..7da359c8f5 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
@@ -327,6 +327,8 @@ public class ZetaSQLType {
             case ZetaSQLFunction.CONCAT_WS:
             case ZetaSQLFunction.HEXTORAW:
             case ZetaSQLFunction.RAWTOHEX:
+            case ZetaSQLFunction.TO_BASE64:
+            case ZetaSQLFunction.FROM_BASE64:
             case ZetaSQLFunction.INSERT:
             case ZetaSQLFunction.LOWER:
             case ZetaSQLFunction.LCASE:
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunction.java
index 51fbe627db..ece5c3c7d1 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunction.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunction.java
@@ -22,12 +22,14 @@ import 
org.apache.seatunnel.shade.com.google.common.hash.Hashing;
 import org.apache.seatunnel.common.exception.CommonErrorCode;
 import org.apache.seatunnel.common.utils.DateTimeUtils;
 import org.apache.seatunnel.common.utils.DateUtils;
+import org.apache.seatunnel.common.utils.EncodingUtils;
 import org.apache.seatunnel.transform.exception.TransformException;
 import org.apache.seatunnel.transform.sql.zeta.ZetaSQLFunction;
 
 import org.apache.groovy.parser.antlr4.util.StringUtils;
 
 import java.lang.reflect.Array;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -36,6 +38,7 @@ import java.time.ZoneOffset;
 import java.time.temporal.Temporal;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -189,6 +192,46 @@ public class StringFunction {
         return buff.toString();
     }
 
+    public static String toBase64(List<Object> args) {
+        if (args.size() > 2) {
+            throw new IllegalArgumentException("TO_BASE64 requires one or two 
arguments");
+        }
+        Object arg = args.get(0);
+        if (arg == null) {
+            return null;
+        }
+        if (arg instanceof byte[]) {
+            if (args.size() == 2) {
+                throw new IllegalArgumentException(
+                        "TO_BASE64 does not support charset for bytes input");
+            }
+            return Base64.getEncoder().encodeToString((byte[]) arg);
+        }
+        Charset charset = getBase64Charset(args);
+        return 
Base64.getEncoder().encodeToString(arg.toString().getBytes(charset));
+    }
+
+    public static String fromBase64(List<Object> args) {
+        if (args.size() > 2) {
+            throw new IllegalArgumentException("FROM_BASE64 requires one or 
two arguments");
+        }
+        Object arg = args.get(0);
+        if (arg == null) {
+            return null;
+        }
+        Charset charset = getBase64Charset(args);
+        try {
+            return new String(Base64.getDecoder().decode(arg.toString()), 
charset);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException("Invalid Base64 content", e);
+        }
+    }
+
+    private static Charset getBase64Charset(List<Object> args) {
+        String charsetName = args.size() == 2 ? (String) args.get(1) : null;
+        return EncodingUtils.tryParseCharset(charsetName);
+    }
+
     public static String insert(List<Object> args) {
         String s1 = (String) args.get(0);
         int start = ((Number) args.get(1)).intValue();
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLStringFunctionsTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLStringFunctionsTest.java
index e63102e19a..3c6c54a6d4 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLStringFunctionsTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLStringFunctionsTest.java
@@ -21,6 +21,7 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig;
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
 import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.PrimitiveByteArrayType;
 import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
@@ -29,6 +30,7 @@ import 
org.apache.seatunnel.transform.exception.TransformException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -293,6 +295,79 @@ public class SQLStringFunctionsTest {
         Assertions.assertEquals("010a", outRow.getField(0));
     }
 
+    @Test
+    public void testToBase64() {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"data"}, new SeaTunnelDataType[] 
{BasicType.STRING_TYPE});
+
+        SeaTunnelRow outRow =
+                runSql(
+                        "select TO_BASE64(data) as encoded_data,"
+                                + " TO_BASE64(data, 'UTF-16') as 
utf16_encoded_data"
+                                + " from dual",
+                        rowType,
+                        "SeaTunnel");
+
+        Assertions.assertEquals("U2VhVHVubmVs", outRow.getField(0));
+        Assertions.assertEquals("/v8AUwBlAGEAVAB1AG4AbgBlAGw=", 
outRow.getField(1));
+    }
+
+    @Test
+    public void testFromBase64() {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"data"}, new SeaTunnelDataType[] 
{BasicType.STRING_TYPE});
+
+        SeaTunnelRow outRow =
+                runSql(
+                        "select FROM_BASE64(data) as decoded_data,"
+                                + " 
FROM_BASE64('/v8AUwBlAGEAVAB1AG4AbgBlAGw=', 'UTF-16') as utf16_decoded_data"
+                                + " from dual",
+                        rowType,
+                        "U2VhVHVubmVs");
+
+        Assertions.assertEquals("SeaTunnel", outRow.getField(0));
+        Assertions.assertEquals("SeaTunnel", outRow.getField(1));
+    }
+
+    @Test
+    public void testToBase64WithBytesColumn() {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"data"},
+                        new SeaTunnelDataType[] 
{PrimitiveByteArrayType.INSTANCE});
+
+        byte[] bytes = "SeaTunnel".getBytes(StandardCharsets.UTF_8);
+        SeaTunnelRow outRow =
+                runSql("select TO_BASE64(data) as encoded_data from dual", 
rowType, bytes);
+
+        Assertions.assertEquals("U2VhVHVubmVs", outRow.getField(0));
+    }
+
+    @Test
+    public void testToBase64BytesColumnRejectsCharset() {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"data"},
+                        new SeaTunnelDataType[] 
{PrimitiveByteArrayType.INSTANCE});
+
+        byte[] bytes = "SeaTunnel".getBytes(StandardCharsets.UTF_8);
+        TransformException exception =
+                Assertions.assertThrows(
+                        TransformException.class,
+                        () ->
+                                runSql(
+                                        "select TO_BASE64(data, 'UTF-16') as 
encoded_data from dual",
+                                        rowType,
+                                        bytes));
+
+        Assertions.assertInstanceOf(IllegalArgumentException.class, 
exception.getCause());
+        Assertions.assertEquals(
+                "TO_BASE64 does not support charset for bytes input",
+                exception.getCause().getMessage());
+    }
+
     @Test
     public void testSoundex() {
         SeaTunnelRowType rowType =
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunctionTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunctionTest.java
index f0e52a2d87..00a858ce4e 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunctionTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/StringFunctionTest.java
@@ -22,6 +22,7 @@ import 
org.apache.seatunnel.transform.exception.TransformException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -29,6 +30,8 @@ import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import java.time.temporal.Temporal;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 
@@ -275,6 +278,114 @@ public class StringFunctionTest {
         Assertions.assertEquals("010a", StringFunction.rawtohex(args));
     }
 
+    @Test
+    public void testToBase64() {
+        List<Object> args = new ArrayList<>();
+        args.add("SeaTunnel");
+        Assertions.assertEquals("U2VhVHVubmVs", StringFunction.toBase64(args));
+
+        args.clear();
+        args.add("hello");
+        args.add("ISO-8859-1");
+        Assertions.assertEquals("aGVsbG8=", StringFunction.toBase64(args));
+
+        args.clear();
+        args.add("SeaTunnel");
+        args.add("UTF-16");
+        Assertions.assertEquals("/v8AUwBlAGEAVAB1AG4AbgBlAGw=", 
StringFunction.toBase64(args));
+    }
+
+    @Test
+    public void testFromBase64() {
+        List<Object> args = new ArrayList<>();
+        args.add("U2VhVHVubmVs");
+        Assertions.assertEquals("SeaTunnel", StringFunction.fromBase64(args));
+
+        args.clear();
+        args.add("aGVsbG8=");
+        args.add("ISO-8859-1");
+        Assertions.assertEquals("hello", StringFunction.fromBase64(args));
+
+        args.clear();
+        args.add("/v8AUwBlAGEAVAB1AG4AbgBlAGw=");
+        args.add("UTF-16");
+        Assertions.assertEquals("SeaTunnel", StringFunction.fromBase64(args));
+    }
+
+    @Test
+    public void testToBase64WithBytesInput() {
+        List<Object> args = new ArrayList<>();
+        args.add("SeaTunnel".getBytes(StandardCharsets.UTF_8));
+        Assertions.assertEquals("U2VhVHVubmVs", StringFunction.toBase64(args));
+    }
+
+    @Test
+    public void testToBase64BytesInputRejectsCharset() {
+        IllegalArgumentException bytesWithCharset =
+                Assertions.assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                StringFunction.toBase64(
+                                        Arrays.asList(
+                                                
"SeaTunnel".getBytes(StandardCharsets.UTF_8),
+                                                "UTF-16")));
+        Assertions.assertEquals(
+                "TO_BASE64 does not support charset for bytes input",
+                bytesWithCharset.getMessage());
+    }
+
+    @Test
+    public void testBase64WithNullInputAndNullCharset() {
+        List<Object> args = new ArrayList<>();
+        args.clear();
+        args.add(null);
+        Assertions.assertNull(StringFunction.toBase64(args));
+        Assertions.assertNull(StringFunction.fromBase64(args));
+
+        Assertions.assertEquals(
+                "U2VhVHVubmVs", 
StringFunction.toBase64(Arrays.asList("SeaTunnel", null)));
+        Assertions.assertEquals(
+                "SeaTunnel", 
StringFunction.fromBase64(Arrays.asList("U2VhVHVubmVs", null)));
+    }
+
+    @Test
+    public void testBase64RejectsInvalidCharset() {
+        Assertions.assertThrows(
+                RuntimeException.class,
+                () -> StringFunction.toBase64(Arrays.asList("SeaTunnel", 
"invalid")));
+        Assertions.assertThrows(
+                RuntimeException.class,
+                () -> StringFunction.fromBase64(Arrays.asList("U2VhVHVubmVs", 
"invalid")));
+    }
+
+    @Test
+    public void testFromBase64RejectsInvalidContent() {
+        IllegalArgumentException invalidBase64 =
+                Assertions.assertThrows(
+                        IllegalArgumentException.class,
+                        () -> 
StringFunction.fromBase64(Collections.singletonList("not-base64!")));
+        Assertions.assertEquals("Invalid Base64 content", 
invalidBase64.getMessage());
+    }
+
+    @Test
+    public void testBase64RejectsInvalidArgumentCount() {
+        IllegalArgumentException invalidToBase64Args =
+                Assertions.assertThrows(
+                        IllegalArgumentException.class,
+                        () -> 
StringFunction.toBase64(Arrays.asList("SeaTunnel", "UTF-8", "x")));
+        Assertions.assertEquals(
+                "TO_BASE64 requires one or two arguments", 
invalidToBase64Args.getMessage());
+
+        IllegalArgumentException invalidFromBase64Args =
+                Assertions.assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                StringFunction.fromBase64(
+                                        Arrays.asList("U2VhVHVubmVs", "UTF-8", 
"x")));
+        Assertions.assertEquals(
+                "FROM_BASE64 requires one or two arguments", 
invalidFromBase64Args.getMessage());
+    }
+
     @Test
     public void testInsertFunction() {
         List<Object> args = new ArrayList<>();

Reply via email to