This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new e3bae975d8d [fix](fe) Preserve Paimon CAST predicate semantics on
branch-4.1 (#67933)
e3bae975d8d is described below
commit e3bae975d8dccb3ba0f1700219e7eb822a8159e2
Author: Gabriel <[email protected]>
AuthorDate: Tue Sep 15 09:24:35 2026 +0800
[fix](fe) Preserve Paimon CAST predicate semantics on branch-4.1 (#67933)
### What problem does this PR solve?
Problem Summary: Paimon predicate conversion on branch-4.1 strips CAST
from column references before building source filters. For a STRING
column containing '05', ' 5', and '5', `CAST(code AS INT) = 5` can
become the source predicate `code = '5'`, incorrectly pruning matching
rows before Doris evaluates the original predicate. Decimal casts that
reduce scale have the same risk.
Only convert bare column references. Keep CAST predicates in Doris,
preserving their value and null semantics. Ordinary column predicates
can still be pushed down independently. This follows the conservative
CAST policy already used by the connector on master.
### Release note
Fix missing rows in Paimon queries with predicates on casted columns on
branch-4.1.
### Check List (For Author)
- Test: FE planner regression tests for CAST comparisons, IN/NOT IN,
null checks, OR, independent AND conjuncts, and ordinary column
pushdown. The 11 predicate-converter tests pass after reproducing 8
failures on the original code. FE Checkstyle passes with 0 violations.
All 59 tests across PaimonPredicateConverterTest, PaimonScanNodeTest,
and PaimonSourceTest pass. No external Paimon cluster was required for
these FE tests.
- Behavior changed: Yes. CAST column predicates remain in Doris instead
of being rewritten into potentially stricter Paimon filters. This may
reduce pushdown for otherwise safe casts.
- Does this need documentation: No.
---
.../paimon/source/PaimonPredicateConverter.java | 11 ++---
.../planner/PaimonPredicateConverterTest.java | 56 ++++++++++++++++++++++
2 files changed, 60 insertions(+), 7 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
index ae45c242718..45b351aa618 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonPredicateConverter.java
@@ -185,15 +185,12 @@ public class PaimonPredicateConverter {
public static SlotRef convertDorisExprToSlotRef(Expr expr) {
- SlotRef slotRef = null;
+ // Stripping CAST can prune matching rows, e.g. CAST('05' AS INT) = 5
is not '05' = '5'.
+ // Keep casted columns in the original Doris conjuncts to preserve
value and null semantics.
if (expr instanceof SlotRef) {
- slotRef = (SlotRef) expr;
- } else if (expr instanceof CastExpr) {
- if (expr.getChild(0) instanceof SlotRef) {
- slotRef = (SlotRef) expr.getChild(0);
- }
+ return (SlotRef) expr;
}
- return slotRef;
+ return null;
}
public LiteralExpr convertDorisExprToLiteralExpr(Expr expr) {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/planner/PaimonPredicateConverterTest.java
b/fe/fe-core/src/test/java/org/apache/doris/planner/PaimonPredicateConverterTest.java
index fde1b6f74c2..85ff507ddb6 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/planner/PaimonPredicateConverterTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/planner/PaimonPredicateConverterTest.java
@@ -17,6 +17,7 @@
package org.apache.doris.planner;
+import org.apache.doris.analysis.CastExpr;
import org.apache.doris.analysis.Expr;
import org.apache.doris.common.FeConstants;
import org.apache.doris.datasource.paimon.source.PaimonPredicateConverter;
@@ -29,10 +30,14 @@ import org.apache.paimon.predicate.LeafPredicate;
import org.apache.paimon.predicate.Or;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DecimalType;
import org.apache.paimon.types.IntType;
import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.VarCharType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
@@ -46,6 +51,57 @@ public class PaimonPredicateConverterTest extends
TestWithFeService {
String tbl1 = "create table db1.tbl1(" + "k1 int," + " k2 int," + " v1
int)" + " distributed by hash(k1)"
+ " properties('replication_num' = '1');";
createTables(tbl1);
+ createTables("create table db1.cast_predicates (id int, code string,
amount decimal(10, 2))"
+ + " distributed by hash(id) properties('replication_num' =
'1')");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"cast(code as int) = 5", "5 = cast(code as int)",
"cast(code as int) > 5",
+ "cast(code as int) in (5, 6)", "cast(code as int) not in (5, 6)",
+ "cast(code as int) is null", "cast(code as int) is not null",
+ "cast(code as int) = 5 or id = 1"})
+ public void rejectStringToIntegerCast(String predicate) throws Exception {
+ List<Expr> conjuncts = planCastPredicate(predicate);
+ Assertions.assertTrue(conjuncts.stream().anyMatch(expr ->
expr.contains(CastExpr.class)), predicate);
+ List<Expr> remaining = Expr.cloneList(conjuncts);
+
Assertions.assertTrue(castPredicateConverter().convertToPaimonExpr(conjuncts).isEmpty(),
predicate);
+ Assertions.assertEquals(remaining, conjuncts);
+ }
+
+ @Test
+ public void rejectDecimalScaleCast() throws Exception {
+ List<Expr> conjuncts = planCastPredicate("cast(amount as decimal(10,
1)) = 1.2");
+ Assertions.assertTrue(conjuncts.stream().anyMatch(expr ->
expr.contains(CastExpr.class)));
+
Assertions.assertTrue(castPredicateConverter().convertToPaimonExpr(conjuncts).isEmpty());
+ }
+
+ @Test
+ public void retainIndependentUncastPredicates() throws Exception {
+ List<Expr> conjuncts = planCastPredicate("cast(code as int) = 5 and id
= 1");
+ List<Expr> remaining = Expr.cloneList(conjuncts);
+ List<Predicate> predicates =
castPredicateConverter().convertToPaimonExpr(conjuncts);
+ Assertions.assertEquals(1, predicates.size());
+ Assertions.assertEquals("id", ((LeafPredicate)
predicates.get(0)).fieldName());
+ Assertions.assertEquals(remaining, conjuncts);
+
+ for (String predicate : Lists.newArrayList("code = '5'", "id > 1", "id
in (1, 2)")) {
+ Assertions.assertEquals(1,
castPredicateConverter().convertToPaimonExpr(planCastPredicate(predicate)).size(),
+ predicate);
+ }
+ }
+
+ private List<Expr> planCastPredicate(String predicate) throws Exception {
+ StmtExecutor executor = new StmtExecutor(connectContext,
+ "select * from db1.cast_predicates where " + predicate);
+ executor.execute();
+ return executor.planner().getScanNodes().get(0).getConjuncts();
+ }
+
+ private PaimonPredicateConverter castPredicateConverter() {
+ return new PaimonPredicateConverter(new RowType(Lists.newArrayList(
+ new DataField(0, "id", new IntType()),
+ new DataField(1, "code", new VarCharType()),
+ new DataField(2, "amount", new DecimalType(10, 2)))));
}
@Test
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]