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 da71a66976 [iceberg] Leave the nanosecond refusal to the validator 
that knows the table (#9503)
da71a66976 is described below

commit da71a669761236c9a85ff37bc552e5a74087326b
Author: Jiajia Li <[email protected]>
AuthorDate: Tue Sep 1 12:21:30 2026 +0800

    [iceberg] Leave the nanosecond refusal to the validator that knows the 
table (#9503)
---
 .../paimon/iceberg/metadata/IcebergDataField.java     | 16 +++++++++-------
 .../org/apache/paimon/schema/SchemaValidation.java    | 13 ++++++++-----
 .../paimon/iceberg/IcebergCompatibilityTest.java      | 19 ++++++++++++++-----
 .../paimon/iceberg/metadata/IcebergDataFieldTest.java | 15 ++++++---------
 4 files changed, 37 insertions(+), 26 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/iceberg/metadata/IcebergDataField.java
 
b/paimon-core/src/main/java/org/apache/paimon/iceberg/metadata/IcebergDataField.java
index 890954aea3..caf5c9aece 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/iceberg/metadata/IcebergDataField.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/iceberg/metadata/IcebergDataField.java
@@ -186,20 +186,22 @@ public class IcebergDataField {
                 return String.format(
                         "decimal(%d, %d)", decimalType.getPrecision(), 
decimalType.getScale());
             case TIMESTAMP_WITHOUT_TIME_ZONE:
+                // Nanoseconds name the Iceberg v3 type. Whether a table may 
publish one is decided
+                // by SchemaValidation#validateIcebergTimestampPrecisions, 
which knows the mirror is
+                // enabled and writes INT96.
                 int timestampPrecision = ((TimestampType) 
dataType).getPrecision();
-                // Paimon writes these as Parquet INT96, which Iceberg reads 
as microseconds
                 Preconditions.checkArgument(
-                        timestampPrecision >= 3 && timestampPrecision <= 6,
+                        timestampPrecision >= 3 && timestampPrecision <= 9,
                         "Paimon Iceberg compatibility only supports timestamp 
types with a "
-                                + "precision from 3 to 6.");
-                return "timestamp";
+                                + "precision from 3 to 9.");
+                return timestampPrecision >= 7 ? "timestamp_ns" : "timestamp";
             case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
                 int timestampLtzPrecision = ((LocalZonedTimestampType) 
dataType).getPrecision();
                 Preconditions.checkArgument(
-                        timestampLtzPrecision >= 3 && timestampLtzPrecision <= 
6,
+                        timestampLtzPrecision >= 3 && timestampLtzPrecision <= 
9,
                         "Paimon Iceberg compatibility only supports timestamp 
types with a "
-                                + "precision from 3 to 6.");
-                return "timestamptz";
+                                + "precision from 3 to 9.");
+                return timestampLtzPrecision >= 7 ? "timestamptz_ns" : 
"timestamptz";
             case VARIANT:
                 return "variant";
             case GEOMETRY:
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java 
b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
index 41834c8d8a..e370e674ae 100644
--- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
+++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
@@ -128,7 +128,10 @@ public class SchemaValidation {
     /** The ceiling {@code IcebergDataField} converts. */
     private static final int MAX_ICEBERG_TIME_PRECISION = 3;
 
-    /** The precisions {@code IcebergDataField} maps to the Iceberg timestamp 
types. */
+    /**
+     * The timestamp precisions the mirror can publish, narrower than the 3 to 
9 {@code
+     * IcebergDataField} names a type for.
+     */
     private static final int MIN_ICEBERG_TIMESTAMP_PRECISION = 3;
 
     private static final int MAX_ICEBERG_TIMESTAMP_PRECISION = 6;
@@ -564,10 +567,10 @@ public class SchemaValidation {
     }
 
     /**
-     * Refuses the timestamp precisions the Iceberg mirror cannot publish, 
matching the range {@link
-     * org.apache.paimon.iceberg.metadata.IcebergDataField} converts. A higher 
precision is written
-     * as Parquet INT96, which Iceberg reads as a microsecond zoned timestamp 
rather than the
-     * nanoseconds the column declares, so the two disagree about the data.
+     * Refuses the timestamp precisions the Iceberg mirror cannot publish. A 
higher precision is
+     * written as Parquet INT96, which Iceberg reads as a microsecond zoned 
timestamp rather than
+     * the nanoseconds the column declares, so the two disagree about the 
data. The refusal belongs
+     * here rather than in the type mapping, which does not know who writes 
the files.
      */
     public static void validateIcebergTimestampPrecisions(DataType dataType, 
CoreOptions options) {
         if 
(options.toConfiguration().get(IcebergOptions.METADATA_ICEBERG_STORAGE)
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
index acaf154edc..050a997d33 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/iceberg/IcebergCompatibilityTest.java
@@ -82,6 +82,7 @@ import org.apache.iceberg.io.CloseableIterable;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 
 import java.io.File;
@@ -106,6 +107,7 @@ import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -1705,10 +1707,18 @@ public class IcebergCompatibilityTest {
                 .hasMessageContaining("precision from 3 to 6");
     }
 
+    /** Below the floor, and above the ceiling in both timestamp families. */
+    static Stream<DataType> unpublishableTimestampTypes() {
+        return Stream.of(
+                DataTypes.TIMESTAMP(2),
+                DataTypes.TIMESTAMP(9),
+                DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(9));
+    }
+
     @ParameterizedTest
-    @ValueSource(ints = {2, 9})
-    public void 
testExistingTableWithUnpublishableHistoricalTimestampsRefusesToCommit(int 
precision)
-            throws Exception {
+    @MethodSource("unpublishableTimestampTypes")
+    public void 
testExistingTableWithUnpublishableHistoricalTimestampsRefusesToCommit(
+            DataType timestampType) throws Exception {
         LocalFileIO fileIO = LocalFileIO.create();
         Path warehouse = new Path(tempDir.toString());
         Options options = new Options();
@@ -1716,8 +1726,7 @@ public class IcebergCompatibilityTest {
         options.set(CoreOptions.FILE_FORMAT, "parquet");
         RowType rowType =
                 RowType.of(
-                        new DataType[] {DataTypes.INT(), 
DataTypes.TIMESTAMP(precision)},
-                        new String[] {"k", "ts"});
+                        new DataType[] {DataTypes.INT(), timestampType}, new 
String[] {"k", "ts"});
         Schema schema =
                 new Schema(
                         rowType.getFields(),
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/iceberg/metadata/IcebergDataFieldTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/iceberg/metadata/IcebergDataFieldTest.java
index eafa732d20..04add79ace 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/iceberg/metadata/IcebergDataFieldTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/iceberg/metadata/IcebergDataFieldTest.java
@@ -226,19 +226,16 @@ class IcebergDataFieldTest {
         IcebergDataField icebergTimestampLtz = new 
IcebergDataField(timestampLtzField);
         assertThat(icebergTimestampLtz.type()).isEqualTo("timestamptz");
 
+        // Nanoseconds name the Iceberg v3 type; SchemaValidation decides who 
may publish one.
         for (int precision = 7; precision <= 9; precision++) {
             DataField nanosField =
                     new DataField(3, "timestamp_ns", new TimestampType(false, 
precision));
-            assertThatThrownBy(() -> new IcebergDataField(nanosField))
-                    .isInstanceOf(IllegalArgumentException.class)
-                    .hasMessageContaining("precision from 3 to 6");
+            assertThat(new 
IcebergDataField(nanosField).type()).isEqualTo("timestamp_ns");
 
             DataField nanosLtzField =
                     new DataField(
                             4, "timestamptz_ns", new 
LocalZonedTimestampType(false, precision));
-            assertThatThrownBy(() -> new IcebergDataField(nanosLtzField))
-                    .isInstanceOf(IllegalArgumentException.class)
-                    .hasMessageContaining("precision from 3 to 6");
+            assertThat(new 
IcebergDataField(nanosLtzField).type()).isEqualTo("timestamptz_ns");
         }
     }
 
@@ -250,21 +247,21 @@ class IcebergDataFieldTest {
                 new DataField(1, "timestamp", new TimestampType(false, 2));
         assertThatThrownBy(() -> new IcebergDataField(invalidTimestampField))
                 .isInstanceOf(IllegalArgumentException.class)
-                .hasMessageContaining("precision from 3 to 6");
+                .hasMessageContaining("precision from 3 to 9");
 
         // Test invalid precision (<= 3)
         DataField invalidTimestampField2 =
                 new DataField(2, "timestamp", new TimestampType(false, 2));
         assertThatThrownBy(() -> new IcebergDataField(invalidTimestampField2))
                 .isInstanceOf(IllegalArgumentException.class)
-                .hasMessageContaining("precision from 3 to 6");
+                .hasMessageContaining("precision from 3 to 9");
 
         // Test invalid local timezone timestamp precision (<= 3)
         DataField invalidTimestampLtzField =
                 new DataField(3, "timestamptz", new 
LocalZonedTimestampType(false, 2));
         assertThatThrownBy(() -> new 
IcebergDataField(invalidTimestampLtzField))
                 .isInstanceOf(IllegalArgumentException.class)
-                .hasMessageContaining("precision from 3 to 6");
+                .hasMessageContaining("precision from 3 to 9");
 
         // Test valid precision boundaries
         DataField validTimestamp4 = new DataField(4, "timestamp", new 
TimestampType(false, 4));

Reply via email to