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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12618-31100cb5301274fdb1f27ba539d2f46ade44dd9e
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit a4c291d8fd6c7b785b90334d4ec2ee68e026dd7a
Author: Sepuri Sai Krishna <[email protected]>
AuthorDate: Mon Oct 12 05:27:05 2026 +0000

    [Fix][Transform-V2] Make calcite, nlpmodel and validator case conversion 
locale independent (#12618)
---
 .../transform/calcite/udf/BuiltinFunctions.java    |  3 +-
 .../calcite/udf/VectorReduceFunction.java          |  3 +-
 .../nlpmodel/ModelInvocationCacheKey.java          |  3 +-
 .../embedding/multimodal/ModalityType.java         |  7 ++-
 .../embedding/multimodal/PayloadFormat.java        |  4 +-
 .../embedding/remote/doubao/DoubaoModel.java       |  3 +-
 .../nlpmodel/llm/remote/AbstractModel.java         |  3 +-
 .../validator/DataValidatorTransformConfig.java    |  5 +-
 .../transform/calcite/CalciteSQLEngineTest.java    | 27 +++++++++
 .../embedding/DoubaoMultimodalModelTest.java       | 68 ++++++++++++++++++++++
 .../nlpmodel/ModelInvocationCacheKeyTest.java      | 35 +++++++++++
 11 files changed, 150 insertions(+), 11 deletions(-)

diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/BuiltinFunctions.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/BuiltinFunctions.java
index d88eb7e72d..f878d0de6f 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/BuiltinFunctions.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/BuiltinFunctions.java
@@ -27,6 +27,7 @@ import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.ServiceLoader;
 
 /** Discovers {@link CalciteUdf} implementations via {@link ServiceLoader} and 
registers them. */
@@ -51,7 +52,7 @@ public final class BuiltinFunctions {
                                         udf.getClass().getName());
                                 return;
                             }
-                            String name = rawName.toUpperCase();
+                            String name = rawName.toUpperCase(Locale.ROOT);
                             boolean opened = false;
                             try {
                                 Method evalMethod = 
findStaticEvalMethod(udf.getClass());
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/VectorReduceFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/VectorReduceFunction.java
index 6bc2655f68..1c0cbdc9af 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/VectorReduceFunction.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/VectorReduceFunction.java
@@ -22,6 +22,7 @@ import org.apache.seatunnel.common.utils.VectorUtils;
 import com.google.auto.service.AutoService;
 
 import java.nio.ByteBuffer;
+import java.util.Locale;
 import java.util.Random;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -57,7 +58,7 @@ public class VectorReduceFunction implements CalciteUdf {
         }
 
         Float[] result;
-        switch (method.toUpperCase()) {
+        switch (method.toUpperCase(Locale.ROOT)) {
             case "TRUNCATE":
                 result = new Float[targetDimension];
                 System.arraycopy(source, 0, result, 0, targetDimension);
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKey.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKey.java
index d9ae64dfba..df2a4dd3d0 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKey.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKey.java
@@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
 
@@ -141,7 +142,7 @@ public final class ModelInvocationCacheKey {
         if (normalized.isEmpty()) {
             return null;
         }
-        return normalized.toLowerCase();
+        return normalized.toLowerCase(Locale.ROOT);
     }
 
     private static String normalizeValue(Object value) {
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/ModalityType.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/ModalityType.java
index 0d5f337348..c40232e899 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/ModalityType.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/ModalityType.java
@@ -23,6 +23,7 @@ import lombok.ToString;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 
 /** Enumeration for multimodal modality types supported by embedding models */
 @AllArgsConstructor
@@ -57,7 +58,7 @@ public enum ModalityType {
             return null;
         }
 
-        String trimmedName = name.trim().toLowerCase();
+        String trimmedName = name.trim().toLowerCase(Locale.ROOT);
         for (ModalityType type : ModalityType.values()) {
             if (type.name.equalsIgnoreCase(trimmedName)) {
                 return type;
@@ -75,7 +76,7 @@ public enum ModalityType {
         if (value == null || value.trim().isEmpty()) {
             return null;
         }
-        String trimmedValue = value.trim().toLowerCase();
+        String trimmedValue = value.trim().toLowerCase(Locale.ROOT);
         String extension = "";
         int lastDotIndex = trimmedValue.lastIndexOf('.');
         if (lastDotIndex > 0 && lastDotIndex < trimmedValue.length() - 1) {
@@ -99,7 +100,7 @@ public enum ModalityType {
         if (extension == null) {
             return false;
         }
-        return fileExtensions.contains(extension.toLowerCase());
+        return fileExtensions.contains(extension.toLowerCase(Locale.ROOT));
     }
 
     public enum ModalityGroup {
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/PayloadFormat.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/PayloadFormat.java
index 60adcb3f4f..dd0ee8bd43 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/PayloadFormat.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/multimodal/PayloadFormat.java
@@ -21,6 +21,8 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.ToString;
 
+import java.util.Locale;
+
 /** Enumeration for data formats supported by multimodal embedding models */
 @AllArgsConstructor
 @Getter
@@ -37,7 +39,7 @@ public enum PayloadFormat {
             return URL;
         }
         for (PayloadFormat format : PayloadFormat.values()) {
-            if (format.name.equalsIgnoreCase(name.trim().toLowerCase())) {
+            if 
(format.name.equalsIgnoreCase(name.trim().toLowerCase(Locale.ROOT))) {
                 return format;
             }
         }
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/remote/doubao/DoubaoModel.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/remote/doubao/DoubaoModel.java
index f175b7c9b2..17ba5b502c 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/remote/doubao/DoubaoModel.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/remote/doubao/DoubaoModel.java
@@ -48,6 +48,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 
 public class DoubaoModel extends MultimodalModel {
 
@@ -385,7 +386,7 @@ public class DoubaoModel extends MultimodalModel {
                 fieldValue =
                         String.format(
                                 BASE64_PARAM_TEMPLATE,
-                                
fieldSpecModalityType.getGroup().name().toLowerCase(),
+                                
fieldSpecModalityType.getGroup().name().toLowerCase(Locale.ROOT),
                                 fieldSpecModalityType.getName(),
                                 srcField.toBase64());
             }
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/remote/AbstractModel.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/remote/AbstractModel.java
index f57d66f906..877ed3f80b 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/remote/AbstractModel.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/remote/AbstractModel.java
@@ -31,6 +31,7 @@ import org.apache.seatunnel.format.json.RowToJsonConverters;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 
 public abstract class AbstractModel implements Model {
 
@@ -120,6 +121,6 @@ public abstract class AbstractModel implements Model {
             throws IOException;
 
     protected String convertData(String data) {
-        return outputType == SqlType.BOOLEAN ? data.toLowerCase() : data;
+        return outputType == SqlType.BOOLEAN ? data.toLowerCase(Locale.ROOT) : 
data;
     }
 }
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformConfig.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformConfig.java
index 2bdbb742b8..d6be1ad5e9 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformConfig.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformConfig.java
@@ -41,6 +41,7 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 @Data
@@ -165,7 +166,7 @@ public class DataValidatorTransformConfig implements 
Serializable {
             return null;
         }
 
-        String ruleType = String.valueOf(ruleTypeObj).toUpperCase();
+        String ruleType = String.valueOf(ruleTypeObj).toUpperCase(Locale.ROOT);
 
         try {
             switch (ruleType) {
@@ -333,7 +334,7 @@ public class DataValidatorTransformConfig implements 
Serializable {
         if (value instanceof Boolean) {
             return (Boolean) value;
         }
-        String stringValue = String.valueOf(value).trim().toLowerCase();
+        String stringValue = 
String.valueOf(value).trim().toLowerCase(Locale.ROOT);
         return "true".equals(stringValue) || "1".equals(stringValue) || 
"yes".equals(stringValue);
     }
 
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/calcite/CalciteSQLEngineTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/calcite/CalciteSQLEngineTest.java
index d9245aa327..efbb2e43e7 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/calcite/CalciteSQLEngineTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/calcite/CalciteSQLEngineTest.java
@@ -40,6 +40,7 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 class CalciteSQLEngineTest {
@@ -4249,4 +4250,30 @@ class CalciteSQLEngineTest {
         Assertions.assertEquals("42", output.getOptions().get("offset"));
         engine.close();
     }
+
+    @Test
+    void testVectorReduceMethodIsLocaleIndependent() {
+        // VECTOR_REDUCE upper-cases the method before matching it, so a 
lower-case method must
+        // resolve. Under a Turkish default locale "random_projection" 
upper-cases to
+        // "RANDOM_PROJECTION" only when the conversion is locale independent; 
otherwise the i
+        // becomes a dotted capital I, no case matches and eval throws.
+        Locale original = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            // try-with-resources rather than a trailing close(), so a failed 
assertion below
+            // still releases the engine.
+            try (CalciteSQLEngine engine =
+                    createAndInit(
+                            "SELECT VECTOR_REDUCE(vec, 2, 'random_projection') 
AS reduced FROM t",
+                            "t",
+                            singleVectorRowType())) {
+                Object result =
+                        singleField(engine, new Object[] {floatVec(1.0f, 2.0f, 
3.0f, 4.0f)});
+                Assertions.assertNotNull(result);
+                Assertions.assertEquals(2, 
VectorUtils.toFloatArray((ByteBuffer) result).length);
+            }
+        } finally {
+            Locale.setDefault(original);
+        }
+    }
 }
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/embedding/DoubaoMultimodalModelTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/embedding/DoubaoMultimodalModelTest.java
index 0489a76ace..5de86b42c4 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/embedding/DoubaoMultimodalModelTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/embedding/DoubaoMultimodalModelTest.java
@@ -36,6 +36,7 @@ import java.util.Base64;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ThreadLocalRandom;
 
@@ -472,4 +473,71 @@ public class DoubaoMultimodalModelTest {
         ObjectNode inputNode = (ObjectNode) result.get("input").get(0);
         Assertions.assertEquals("image_url", inputNode.get("type").asText());
     }
+
+    @Test
+    void testBinaryBase64MimeTypeIsLocaleIndependent() {
+        // The base64 payload embeds the modality group as a MIME type. 
Lower-casing IMAGE under a
+        // Turkish default locale yields a dotless i, so the wire value would 
carry a mangled
+        // MIME type instead of "data:image/png".
+        Locale original = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            Map<String, Object> fieldConfig = new HashMap<>();
+            fieldConfig.put("field", "binary_image_field");
+            fieldConfig.put("modality", "png");
+            fieldConfig.put("format", "binary");
+            Map.Entry<String, Object> entry =
+                    new 
java.util.AbstractMap.SimpleEntry<>("binary_image_vector", fieldConfig);
+
+            VectorFieldSpec vectorFieldSpec = new VectorFieldSpec(entry);
+            byte[] imageData = 
"mock-image-data".getBytes(java.nio.charset.StandardCharsets.UTF_8);
+            MultimodalFieldValue value =
+                    new MultimodalFieldValue(
+                            Collections.singletonList(
+                                    new SrcField(
+                                            
vectorFieldSpec.getSrcFieldSpecs().get(0), imageData)));
+
+            ObjectNode result = model.multimodalBody(value);
+            String url =
+                    ((ObjectNode) 
result.get("input").get(0)).get("image_url").get("url").asText();
+            Assertions.assertTrue(
+                    url.startsWith("data:image/png;base64,"),
+                    "MIME type must not depend on the default locale, but was: 
"
+                            + url.substring(0, Math.min(url.length(), 24)));
+            
Assertions.assertTrue(url.endsWith(Base64.getEncoder().encodeToString(imageData)));
+        } finally {
+            Locale.setDefault(original);
+        }
+    }
+
+    @Test
+    void testFileSuffixModalityDetectionIsLocaleIndependent() {
+        // An upper-case suffix is lower-cased before it is matched against 
the extension list.
+        // Under a Turkish default locale the I becomes a dotless i, so the 
suffix matches no
+        // known extension and the modality is silently not detected.
+        Locale original = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            Map<String, Object> fieldConfig = new HashMap<>();
+            fieldConfig.put("field", "image_field");
+            fieldConfig.put("format", "url");
+            Map.Entry<String, Object> entry =
+                    new java.util.AbstractMap.SimpleEntry<>("image_vector", 
fieldConfig);
+
+            VectorFieldSpec vectorFieldSpec = new VectorFieldSpec(entry);
+            MultimodalFieldValue value =
+                    new MultimodalFieldValue(
+                            Collections.singletonList(
+                                    new SrcField(
+                                            
vectorFieldSpec.getSrcFieldSpecs().get(0),
+                                            "https://example.com/photo.GIF";)));
+
+            Assertions.assertEquals(
+                    ModalityType.GIF, 
value.getSrcFields().get(0).getFieldSpec().getModalityType());
+            Assertions.assertTrue(ModalityType.TIFF.supportsExtension("TIF"));
+            Assertions.assertTrue(ModalityType.AVI.supportsExtension("AVI"));
+        } finally {
+            Locale.setDefault(original);
+        }
+    }
 }
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKeyTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKeyTest.java
index 0de16278e5..3c054dcbdc 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKeyTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/nlpmodel/ModelInvocationCacheKeyTest.java
@@ -20,6 +20,8 @@ package org.apache.seatunnel.transform.nlpmodel;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.Locale;
+
 public class ModelInvocationCacheKeyTest {
 
     @Test
@@ -141,4 +143,37 @@ public class ModelInvocationCacheKeyTest {
                 .input("same chunk")
                 .build();
     }
+
+    @Test
+    void keyIsStableAcrossSpellingsUnderAnyDefaultLocale() {
+        // Tokens are lower-cased so that equivalent spellings collapse to one 
key. Under a Turkish
+        // default locale the I in "OpenAI" lower-cases to a dotless i while 
"openai" is already
+        // lower case, so two equivalent configurations would produce two 
different keys.
+        Locale original = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            String mixedCase =
+                    ModelInvocationCacheKey.builder()
+                            .provider("OpenAI")
+                            .model("text-embedding-3-small")
+                            .dimension(1536)
+                            .modality("Image")
+                            .format("BINARY")
+                            .input("same chunk")
+                            .build();
+            String lowerCase =
+                    ModelInvocationCacheKey.builder()
+                            .provider("openai")
+                            .model("text-embedding-3-small")
+                            .dimension(1536)
+                            .modality("image")
+                            .format("binary")
+                            .input("same chunk")
+                            .build();
+
+            Assertions.assertEquals(mixedCase, lowerCase);
+        } finally {
+            Locale.setDefault(original);
+        }
+    }
 }

Reply via email to