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<>();