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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 9f64a2d1b7 [common] Fix NullPointerExceptions on null transform inputs 
(#9214)
9f64a2d1b7 is described below

commit 9f64a2d1b78284fd3493ed9818ad2e7aa858573a
Author: Jiajia Li <[email protected]>
AuthorDate: Thu Aug 20 10:31:21 2026 +0800

    [common] Fix NullPointerExceptions on null transform inputs (#9214)
---
 .../apache/paimon/predicate/StringTransform.java   |  2 +-
 .../paimon/predicate/SubstringTransform.java       | 51 ++++++++++++++--------
 .../org/apache/paimon/predicate/TrimTransform.java |  9 +++-
 .../paimon/predicate/ConcatTransformTest.java      |  9 ++++
 .../paimon/predicate/SubstringTransformTest.java   | 42 ++++++++++++++++++
 .../apache/paimon/predicate/TrimTransformTest.java | 12 +++++
 6 files changed, 105 insertions(+), 20 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/predicate/StringTransform.java 
b/paimon-common/src/main/java/org/apache/paimon/predicate/StringTransform.java
index 7b87a67c2c..1dd2405ca6 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/predicate/StringTransform.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/predicate/StringTransform.java
@@ -158,7 +158,7 @@ public abstract class StringTransform implements Transform {
     @Override
     public String toString() {
         List<String> inputs =
-                
this.inputs.stream().map(Object::toString).collect(Collectors.toList());
+                
this.inputs.stream().map(String::valueOf).collect(Collectors.toList());
         return name() + "(" + String.join(", ", inputs) + ')';
     }
 }
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/predicate/SubstringTransform.java
 
b/paimon-common/src/main/java/org/apache/paimon/predicate/SubstringTransform.java
index 054422a201..9094837d72 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/predicate/SubstringTransform.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/predicate/SubstringTransform.java
@@ -65,30 +65,25 @@ public class SubstringTransform implements Transform {
             return sourceString;
         }
 
-        String sourceJavaString = sourceString.toString();
-        Object begin = inputs.get(1);
-        int beginIndex;
-        if (begin instanceof FieldRef) {
-            FieldRef beginRef = (FieldRef) begin;
-            checkArgument(beginRef.type().is(INTEGER_NUMERIC));
-            beginIndex = row.getInt(beginRef.index());
-        } else {
-            beginIndex = Integer.parseInt(inputs.get(1).toString());
+        // SQL null propagation: any null input yields null, whether it 
arrives as a
+        // literal or as a null value in a referenced field
+        if (isNullPosition(inputs.get(1), row)) {
+            return null;
+        }
+        boolean hasLength = inputs.size() == 3;
+        if (hasLength && isNullPosition(inputs.get(2), row)) {
+            return null;
         }
+
+        String sourceJavaString = sourceString.toString();
+        int beginIndex = readPosition(inputs.get(1), row);
         if (beginIndex > sourceJavaString.length()) {
             return BinaryString.EMPTY_UTF8;
         }
 
         int endIndex = sourceJavaString.length();
-        if (inputs.size() == 3) {
-            Object end = inputs.get(2);
-            if (end instanceof FieldRef) {
-                FieldRef endRef = (FieldRef) inputs.get(2);
-                checkArgument(endRef.type().is(INTEGER_NUMERIC));
-                endIndex = beginIndex + row.getInt(endRef.index()) - 1;
-            } else {
-                endIndex = beginIndex + 
Integer.parseInt(inputs.get(2).toString()) - 1;
-            }
+        if (hasLength) {
+            endIndex = beginIndex + readPosition(inputs.get(2), row) - 1;
         }
         endIndex = Math.min(endIndex, sourceJavaString.length());
         beginIndex--;
@@ -97,6 +92,26 @@ public class SubstringTransform implements Transform {
         return BinaryString.fromString(sourceJavaString.substring(beginIndex, 
endIndex));
     }
 
+    private static boolean isNullPosition(Object position, InternalRow row) {
+        if (position == null) {
+            return true;
+        }
+        if (position instanceof FieldRef) {
+            FieldRef ref = (FieldRef) position;
+            checkArgument(ref.type().is(INTEGER_NUMERIC));
+            // getInt on a null throws on GenericRow and reads an undefined 
value on columnar rows
+            return row.isNullAt(ref.index());
+        }
+        return false;
+    }
+
+    private static int readPosition(Object position, InternalRow row) {
+        if (position instanceof FieldRef) {
+            return row.getInt(((FieldRef) position).index());
+        }
+        return Integer.parseInt(position.toString());
+    }
+
     @Override
     public Transform copyWithNewInputs(List<Object> inputs) {
         return new SubstringTransform(inputs);
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/predicate/TrimTransform.java 
b/paimon-common/src/main/java/org/apache/paimon/predicate/TrimTransform.java
index 6182335bb2..01ba75e729 100644
--- a/paimon-common/src/main/java/org/apache/paimon/predicate/TrimTransform.java
+++ b/paimon-common/src/main/java/org/apache/paimon/predicate/TrimTransform.java
@@ -51,7 +51,14 @@ public class TrimTransform extends StringTransform {
             return null;
         }
         String sourceString = inputs.get(0).toString();
-        String charsToTrim = inputs.size() == 1 ? " " : 
inputs.get(1).toString();
+        String charsToTrim = " ";
+        if (inputs.size() == 2) {
+            if (inputs.get(1) == null) {
+                // StringUtils.ltrim/rtrim treat a null charsToTrim as a null 
result
+                return null;
+            }
+            charsToTrim = inputs.get(1).toString();
+        }
         switch (trimFlag) {
             case BOTH:
                 return BinaryString.fromString(StringUtils.trim(sourceString, 
charsToTrim));
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/predicate/ConcatTransformTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/predicate/ConcatTransformTest.java
index e776040f89..a9cc9e7a55 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/predicate/ConcatTransformTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/predicate/ConcatTransformTest.java
@@ -72,4 +72,13 @@ class ConcatTransformTest {
                                 BinaryString.fromString("-he")));
         assertThat(result).isEqualTo(BinaryString.fromString("ha-he"));
     }
+
+    @Test
+    public void testToStringWithNullInput() {
+        List<Object> inputs = new ArrayList<>();
+        inputs.add(BinaryString.fromString("a"));
+        inputs.add(null);
+
+        assertThat(new 
ConcatTransform(inputs).toString()).isEqualTo("CONCAT(a, null)");
+    }
 }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/predicate/SubstringTransformTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/predicate/SubstringTransformTest.java
index b4d998bea9..9dd80a5f94 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/predicate/SubstringTransformTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/predicate/SubstringTransformTest.java
@@ -100,6 +100,48 @@ class SubstringTransformTest {
         assertThat(result).isEqualTo(BinaryString.fromString("ell"));
     }
 
+    @Test
+    public void testNullPositionYieldsNull() {
+        List<Object> literal = new ArrayList<>();
+        literal.add(BinaryString.fromString("123"));
+        literal.add(null);
+        assertThat(new 
SubstringTransform(literal).transform(GenericRow.of())).isNull();
+
+        literal.set(1, 1);
+        literal.add(null);
+        assertThat(new 
SubstringTransform(literal).transform(GenericRow.of())).isNull();
+
+        // a null length propagates even when begin is past the end, which on 
its own
+        // would have yielded an empty string
+        literal.set(1, 99);
+        assertThat(new 
SubstringTransform(literal).transform(GenericRow.of())).isNull();
+
+        // and it is found before the malformed begin next to it is parsed
+        literal.set(1, BinaryString.fromString("bad"));
+        assertThat(new 
SubstringTransform(literal).transform(GenericRow.of())).isNull();
+    }
+
+    @Test
+    public void testNullPositionFieldYieldsNull() {
+        List<Object> inputs = new ArrayList<>();
+        inputs.add(new FieldRef(0, "f0", DataTypes.STRING()));
+        inputs.add(new FieldRef(1, "f1", DataTypes.INT()));
+        assertThat(
+                        new SubstringTransform(inputs)
+                                .transform(
+                                        GenericRow.of(
+                                                
BinaryString.fromString("123-45-6789"), null)))
+                .isNull();
+
+        inputs.add(new FieldRef(2, "f2", DataTypes.INT()));
+        assertThat(
+                        new SubstringTransform(inputs)
+                                .transform(
+                                        GenericRow.of(
+                                                
BinaryString.fromString("123-45-6789"), 8, null)))
+                .isNull();
+    }
+
     @Test
     public void testSubstringRefInputUsesSourceFieldNullability() {
         List<Object> inputs = new ArrayList<>();
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/predicate/TrimTransformTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/predicate/TrimTransformTest.java
index b24fda78a7..597a71d280 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/predicate/TrimTransformTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/predicate/TrimTransformTest.java
@@ -101,6 +101,18 @@ class TrimTransformTest {
         assertThat(result).isEqualTo(BinaryString.fromString(" aa"));
     }
 
+    @Test
+    public void testNullCharsToTrimYieldsNull() {
+        List<Object> inputs = new ArrayList<>();
+        inputs.add(new FieldRef(0, "f0", DataTypes.STRING()));
+        inputs.add(new FieldRef(1, "f1", DataTypes.STRING()));
+        GenericRow row = GenericRow.of(BinaryString.fromString("  x  "), null);
+
+        for (TrimTransform.Flag flag : TrimTransform.Flag.values()) {
+            assertThat(new TrimTransform(inputs, 
flag).transform(row)).isNull();
+        }
+    }
+
     @Test
     public void testSubstringRefInputs() {
         List<Object> inputs = new ArrayList<>();

Reply via email to