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 ca15be8fe0 [common] Fix JSON round-trip of temporal and decimal 
predicate literals (#9640)
ca15be8fe0 is described below

commit ca15be8fe04b5a097e286df77a3ab91b3c01560f
Author: YangJie <[email protected]>
AuthorDate: Sun Sep 13 22:35:40 2026 -0400

    [common] Fix JSON round-trip of temporal and decimal predicate literals 
(#9640)
---
 .../requests/ListPartitionsByFilterRequest.java    | 10 +++-
 .../org/apache/paimon/predicate/LeafPredicate.java | 55 +++++++++++++++++++++-
 .../paimon/predicate/PredicateJsonSerdeTest.java   | 44 +++++++++++++++++
 .../org/apache/paimon/rest/RESTApiJsonTest.java    | 52 ++++++++++++++++++++
 4 files changed, 158 insertions(+), 3 deletions(-)

diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
index 102b51e11d..8af21306f0 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/ListPartitionsByFilterRequest.java
@@ -40,7 +40,15 @@ public class ListPartitionsByFilterRequest implements 
RESTRequest {
     private static final String FIELD_MAX_RESULTS = "maxResults";
     private static final String FIELD_PAGE_TOKEN = "pageToken";
 
-    /** JSON serialization of a Paimon {@code Predicate} tree over the 
partition columns. */
+    /**
+     * JSON serialization of a Paimon {@code Predicate} tree over the 
partition columns.
+     *
+     * <p>Wire encoding of the literals a server has to parse: DATE, TIME, 
TIMESTAMP and
+     * TIMESTAMP_LTZ are ISO-8601 strings (e.g. {@code "2026-01-15"}, {@code 
"12:34:56.789"}, {@code
+     * "2026-01-15T12:34:56.789"}, {@code "2026-01-15T04:34:56.789Z"}); 
DECIMAL is a plain
+     * (non-scientific) decimal string. Older clients emitted these as JSON 
arrays and numbers that
+     * no Paimon server could read back, so no previously working request 
encoding changes meaning.
+     */
     @JsonProperty(FIELD_FILTER)
     private final String filter;
 
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/predicate/LeafPredicate.java 
b/paimon-common/src/main/java/org/apache/paimon/predicate/LeafPredicate.java
index 1383837f93..85a0485539 100644
--- a/paimon-common/src/main/java/org/apache/paimon/predicate/LeafPredicate.java
+++ b/paimon-common/src/main/java/org/apache/paimon/predicate/LeafPredicate.java
@@ -35,6 +35,11 @@ import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonPro
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -262,11 +267,30 @@ public class LeafPredicate implements Predicate {
         }
         List<Object> serialized = new ArrayList<>(literals.size());
         for (Object lit : literals) {
-            serialized.add(PredicateBuilder.convertToJavaObject(type, lit));
+            
serialized.add(toJsonFriendly(PredicateBuilder.convertToJavaObject(type, lit)));
         }
         return serialized;
     }
 
+    /**
+     * Temporal and decimal literals are carried as strings: reading the JSON 
back into the untyped
+     * literal list materializes Jackson's JavaTimeModule array/number forms 
as {@code List}/{@code
+     * Double}, which {@link PredicateBuilder#convertJavaObject} rejects, and 
the double
+     * materialization of a decimal is lossy beyond ~15 significant digits.
+     */
+    private static Object toJsonFriendly(Object literal) {
+        if (literal instanceof LocalDate
+                || literal instanceof LocalTime
+                || literal instanceof LocalDateTime
+                || literal instanceof Instant) {
+            return literal.toString();
+        }
+        if (literal instanceof BigDecimal) {
+            return ((BigDecimal) literal).toPlainString();
+        }
+        return literal;
+    }
+
     protected static List<Object> deserializeLiterals(DataType type, 
List<Object> literals) {
         if (literals == null) {
             return null;
@@ -277,8 +301,35 @@ public class LeafPredicate implements Predicate {
                 converted.add(literal);
                 continue;
             }
-            converted.add(PredicateBuilder.convertJavaObject(type, literal));
+            converted.add(
+                    PredicateBuilder.convertJavaObject(type, 
parseJsonLiteral(type, literal)));
         }
         return converted;
     }
+
+    /**
+     * Converts a literal materialized from JSON back into the object {@link
+     * PredicateBuilder#convertJavaObject} accepts, undoing {@link 
#toJsonFriendly}. Anything else
+     * is returned unchanged so that convertJavaObject reports it.
+     */
+    private static Object parseJsonLiteral(DataType type, Object literal) {
+        if (!(literal instanceof String)) {
+            return literal;
+        }
+        String text = (String) literal;
+        switch (type.getTypeRoot()) {
+            case DATE:
+                return LocalDate.parse(text);
+            case TIME_WITHOUT_TIME_ZONE:
+                return LocalTime.parse(text);
+            case TIMESTAMP_WITHOUT_TIME_ZONE:
+                return LocalDateTime.parse(text);
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                return Instant.parse(text);
+            case DECIMAL:
+                return new BigDecimal(text);
+            default:
+                return literal;
+        }
+    }
 }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
index 362f34f917..4a62f0008e 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
@@ -19,16 +19,23 @@
 package org.apache.paimon.predicate;
 
 import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.Timestamp;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.IntType;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.JsonSerdeUtil;
 
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
 import javax.annotation.Nullable;
 
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -272,6 +279,43 @@ class PredicateJsonSerdeTest {
         }
     }
 
+    @Test
+    void testTemporalAndDecimalLiteralsRoundTrip() {
+        Predicate predicate =
+                PredicateBuilder.and(
+                        temporalBuilder().equal(0, (int) LocalDate.of(2026, 1, 
15).toEpochDay()),
+                        temporalBuilder().equal(1, 45_296_789), // 12:34:56.789
+                        temporalBuilder()
+                                .equal(
+                                        2,
+                                        Timestamp.fromLocalDateTime(
+                                                LocalDateTime.of(
+                                                        2026, 1, 15, 12, 34, 
56, 789_000_000))),
+                        temporalBuilder()
+                                .equal(
+                                        3,
+                                        Timestamp.fromInstant(
+                                                
Instant.parse("2026-01-15T04:34:56.789Z"))),
+                        temporalBuilder()
+                                .equal(
+                                        4,
+                                        Decimal.fromBigDecimal(
+                                                new 
BigDecimal("12345678901234567.891"), 20, 3)));
+
+        assertThat(parse(toJson(predicate))).isEqualTo(predicate);
+    }
+
+    private static PredicateBuilder temporalBuilder() {
+        return new PredicateBuilder(
+                RowType.of(
+                        DataTypes.DATE(),
+                        DataTypes.TIME(3),
+                        DataTypes.TIMESTAMP(6),
+                        DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(9),
+                        DataTypes.DECIMAL(20, 3),
+                        DataTypes.STRING()));
+    }
+
     private static PredicateBuilder newBuilder() {
         return new PredicateBuilder(
                 RowType.of(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
index e16233ece5..6ed35b84d4 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
@@ -18,8 +18,12 @@
 
 package org.apache.paimon.rest;
 
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.Timestamp;
 import org.apache.paimon.function.FunctionChange;
 import org.apache.paimon.partition.PartitionStatistics;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.rest.requests.AlterDatabaseRequest;
 import org.apache.paimon.rest.requests.AlterFunctionRequest;
 import org.apache.paimon.rest.requests.AlterTableRequest;
@@ -53,6 +57,8 @@ import org.apache.paimon.table.Instant;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.JsonSerdeUtil;
 import org.apache.paimon.view.ViewChange;
 
 import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
@@ -292,6 +298,52 @@ public class RESTApiJsonTest {
                         Map.class));
     }
 
+    @Test
+    public void 
listPartitionsByFilterRequestPreservesTemporalAndDecimalLiterals()
+            throws Exception {
+        // A partition filter reaches the server as 
JsonSerdeUtil.toFlatJson(predicate) carried in
+        // the request's filter field. 
DATE/TIME/TIMESTAMP/TIMESTAMP_LTZ/DECIMAL literals travel as
+        // strings; assert they survive the full request round-trip that a 
server parses, including
+        // a decimal with more significant digits than a double can hold.
+        PredicateBuilder builder =
+                new PredicateBuilder(
+                        RowType.of(
+                                DataTypes.DATE(),
+                                DataTypes.TIME(3),
+                                DataTypes.TIMESTAMP(6),
+                                DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(9),
+                                DataTypes.DECIMAL(38, 18)));
+        Predicate predicate =
+                PredicateBuilder.and(
+                        builder.equal(0, (int) java.time.LocalDate.of(2026, 1, 
15).toEpochDay()),
+                        builder.equal(1, 45_296_789), // 12:34:56.789
+                        builder.equal(
+                                2,
+                                Timestamp.fromLocalDateTime(
+                                        java.time.LocalDateTime.of(
+                                                2026, 1, 15, 12, 34, 56, 
789_000_000))),
+                        builder.equal(
+                                3,
+                                Timestamp.fromInstant(
+                                        
java.time.Instant.parse("2026-01-15T04:34:56.789Z"))),
+                        builder.equal(
+                                4,
+                                Decimal.fromBigDecimal(
+                                        new java.math.BigDecimal(
+                                                
"12345678901234567890.123456789012345678"),
+                                        38,
+                                        18)));
+
+        ListPartitionsByFilterRequest request =
+                new ListPartitionsByFilterRequest(
+                        JsonSerdeUtil.toFlatJson(predicate), "dt=2026%", 2, 
null);
+        ListPartitionsByFilterRequest parsed =
+                RESTApi.fromJson(RESTApi.toJson(request), 
ListPartitionsByFilterRequest.class);
+        Predicate serverSide = JsonSerdeUtil.fromJson(parsed.getFilter(), 
Predicate.class);
+
+        assertEquals(predicate, serverSide);
+    }
+
     @Test
     public void createPartitionsResponseParseTest() throws Exception {
         Map<String, String> created = new HashMap<>();

Reply via email to