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 56a5493872 [vortex] Do not push unrepresentable timestamp predicate 
literals (#9675)
56a5493872 is described below

commit 56a5493872ff75455cec969cde538d8d2649599f
Author: jackylee <[email protected]>
AuthorDate: Fri Sep 11 14:02:30 2026 +0800

    [vortex] Do not push unrepresentable timestamp predicate literals (#9675)
---
 .../format/vortex/VortexPredicateConverter.java    | 13 ++++
 .../vortex/VortexPredicateConverterTest.java       | 70 ++++++++++++++++++++++
 2 files changed, 83 insertions(+)

diff --git 
a/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexPredicateConverter.java
 
b/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexPredicateConverter.java
index d957daddfb..a4ec47aba5 100644
--- 
a/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexPredicateConverter.java
+++ 
b/paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexPredicateConverter.java
@@ -180,13 +180,26 @@ public class VortexPredicateConverter implements 
PredicateVisitor<Expression> {
 
     private static Expression toTimestampLiteral(
             Timestamp ts, int precision, @Nullable String timeZone) {
+        // The literal carries the precision of the engine that produced it, 
the file carries the
+        // precision of the column. When the literal does not land exactly on 
the column's grain,
+        // no single rounding direction is right for every operator, so refuse 
to push it down and
+        // let the caller drop the leaf instead.
         if (precision == 0) {
+            if (ts.getNanoOfMillisecond() != 0 || ts.getMillisecond() % 1000 
!= 0) {
+                return null;
+            }
             return Expression.literalTimestamp(
                     ts.getMillisecond() / 1000, Expression.TimeUnit.SECONDS, 
timeZone);
         } else if (precision <= 3) {
+            if (ts.getNanoOfMillisecond() != 0) {
+                return null;
+            }
             return Expression.literalTimestamp(
                     ts.getMillisecond(), Expression.TimeUnit.MILLISECONDS, 
timeZone);
         } else if (precision <= 6) {
+            if (ts.getNanoOfMillisecond() % 1000 != 0) {
+                return null;
+            }
             return Expression.literalTimestamp(
                     ts.getMillisecond() * 1000 + ts.getNanoOfMillisecond() / 
1000,
                     Expression.TimeUnit.MICROSECONDS,
diff --git 
a/paimon-vortex/paimon-vortex-format/src/test/java/org/apache/paimon/format/vortex/VortexPredicateConverterTest.java
 
b/paimon-vortex/paimon-vortex-format/src/test/java/org/apache/paimon/format/vortex/VortexPredicateConverterTest.java
index b36860ae7d..63b2099a57 100644
--- 
a/paimon-vortex/paimon-vortex-format/src/test/java/org/apache/paimon/format/vortex/VortexPredicateConverterTest.java
+++ 
b/paimon-vortex/paimon-vortex-format/src/test/java/org/apache/paimon/format/vortex/VortexPredicateConverterTest.java
@@ -424,6 +424,76 @@ public class VortexPredicateConverterTest {
         assertEquals(2_000_000L, rows.get(0).getTimestamp(0, 
0).getMillisecond());
     }
 
+    @Test
+    public void testTimestampSecondsUnrepresentableLiteralNotPushed(
+            @TempDir java.nio.file.Path tempDir) throws Exception {
+        // The literal carries millisecond precision while the column is 
stored in seconds. No
+        // rounding direction answers every operator, so the leaf is not 
pushed and the reader
+        // returns every row for the engine to filter. Rounding down to 1s 
used to drop the
+        // 1000ms row from "< 1500ms" and the 1000ms row from "!= 1500ms".
+        RowType tsRowType = RowType.builder().field("f_ts", 
DataTypes.TIMESTAMP(0)).build();
+        PredicateBuilder tsBuilder = new PredicateBuilder(tsRowType);
+        GenericRow[] data = {
+            GenericRow.of(Timestamp.fromEpochMillis(1_000L)),
+            GenericRow.of(Timestamp.fromEpochMillis(2_000L)),
+            GenericRow.of(Timestamp.fromEpochMillis(3_000L))
+        };
+
+        assertEquals(
+                3,
+                roundTrip(
+                                tempDir,
+                                tsRowType,
+                                data,
+                                Collections.singletonList(
+                                        tsBuilder.lessThan(0, 
Timestamp.fromEpochMillis(1_500L))))
+                        .size());
+        assertEquals(
+                3,
+                roundTrip(
+                                tempDir,
+                                tsRowType,
+                                data,
+                                Collections.singletonList(
+                                        tsBuilder.notEqual(0, 
Timestamp.fromEpochMillis(1_500L))))
+                        .size());
+    }
+
+    @Test
+    public void testTimestampSecondsPreEpochLiteralNotPushed(@TempDir 
java.nio.file.Path tempDir)
+            throws Exception {
+        // integer division truncates toward zero, so -500ms became -0s and 
dropped the epoch row
+        RowType tsRowType = RowType.builder().field("f_ts", 
DataTypes.TIMESTAMP(0)).build();
+        PredicateBuilder tsBuilder = new PredicateBuilder(tsRowType);
+        List<InternalRow> rows =
+                roundTrip(
+                        tempDir,
+                        tsRowType,
+                        new GenericRow[] 
{GenericRow.of(Timestamp.fromEpochMillis(0L))},
+                        Collections.singletonList(
+                                tsBuilder.greaterThan(0, 
Timestamp.fromEpochMillis(-500L))));
+        assertEquals(1, rows.size());
+        assertEquals(0L, rows.get(0).getTimestamp(0, 0).getMillisecond());
+    }
+
+    @Test
+    public void testTimestampMillisSubMillisecondLiteralNotPushed(
+            @TempDir java.nio.file.Path tempDir) throws Exception {
+        // a TIMESTAMP(3) column is stored in milliseconds, so the sub 
millisecond part of the
+        // literal was silently discarded and the 1500ms row was dropped
+        RowType tsRowType = RowType.builder().field("f_ts", 
DataTypes.TIMESTAMP(3)).build();
+        PredicateBuilder tsBuilder = new PredicateBuilder(tsRowType);
+        List<InternalRow> rows =
+                roundTrip(
+                        tempDir,
+                        tsRowType,
+                        new GenericRow[] 
{GenericRow.of(Timestamp.fromEpochMillis(1_500L))},
+                        Collections.singletonList(
+                                tsBuilder.lessThan(0, 
Timestamp.fromMicros(1_500_500L))));
+        assertEquals(1, rows.size());
+        assertEquals(1_500L, rows.get(0).getTimestamp(0, 0).getMillisecond());
+    }
+
     private List<InternalRow> roundTrip(
             java.nio.file.Path tempDir,
             RowType rowType,

Reply via email to