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 bb5498ff32 [spark] Support timestamp_ntz columns in zorder and hilbert 
clustering (#10011)
bb5498ff32 is described below

commit bb5498ff324f58c53a8292e5c8339222959c61c7
Author: cxzl25 <[email protected]>
AuthorDate: Mon Sep 21 10:42:10 2026 +0800

    [spark] Support timestamp_ntz columns in zorder and hilbert clustering 
(#10011)
---
 .../apache/paimon/spark/sort/SparkHilbertUDF.java  | 17 ++++++
 .../apache/paimon/spark/sort/SparkZOrderUDF.java   | 17 ++++++
 .../paimon/spark/sort/SparkHilbertUDFTest.java     | 57 ++++++++++++++++++++
 .../paimon/spark/sort/SparkZOrderUDFTest.java      | 61 ++++++++++++++++++++++
 4 files changed, 152 insertions(+)

diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkHilbertUDF.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkHilbertUDF.java
index 971e8342e5..0e60000389 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkHilbertUDF.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkHilbertUDF.java
@@ -37,11 +37,14 @@ import org.apache.spark.sql.types.IntegerType;
 import org.apache.spark.sql.types.LongType;
 import org.apache.spark.sql.types.ShortType;
 import org.apache.spark.sql.types.StringType;
+import org.apache.spark.sql.types.TimestampNTZType;
 import org.apache.spark.sql.types.TimestampType;
 
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
 
 import scala.collection.JavaConverters;
 import scala.collection.Seq;
@@ -110,6 +113,18 @@ public class SparkHilbertUDF implements Serializable {
         return udf;
     }
 
+    private UserDefinedFunction timestampNtzToLongUDF() {
+        UserDefinedFunction udf =
+                functions
+                        .udf(
+                                (LocalDateTime value) ->
+                                        value == null ? null : 
value.toEpochSecond(ZoneOffset.UTC),
+                                DataTypes.LongType)
+                        .withName("TIMESTAMP_NTZ_LONG");
+
+        return udf;
+    }
+
     private UserDefinedFunction longToOrderedLongUDF() {
         UserDefinedFunction udf =
                 functions
@@ -227,6 +242,8 @@ public class SparkHilbertUDF implements Serializable {
             return booleanToOrderedLongUDF().apply(column);
         } else if (type instanceof TimestampType) {
             return 
longToOrderedLongUDF().apply(column.cast(DataTypes.LongType));
+        } else if (type instanceof TimestampNTZType) {
+            return 
longToOrderedLongUDF().apply(timestampNtzToLongUDF().apply(column));
         } else if (type instanceof DecimalType) {
             return 
longToOrderedLongUDF().apply(column.cast(DataTypes.LongType));
         } else if (type instanceof DateType) {
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkZOrderUDF.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkZOrderUDF.java
index fbd944e9a1..7bb80daaf3 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkZOrderUDF.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/sort/SparkZOrderUDF.java
@@ -35,10 +35,13 @@ import org.apache.spark.sql.types.IntegerType;
 import org.apache.spark.sql.types.LongType;
 import org.apache.spark.sql.types.ShortType;
 import org.apache.spark.sql.types.StringType;
+import org.apache.spark.sql.types.TimestampNTZType;
 import org.apache.spark.sql.types.TimestampType;
 
 import java.io.Serializable;
 import java.nio.ByteBuffer;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
 
 import scala.collection.JavaConverters;
 import scala.collection.Seq;
@@ -166,6 +169,18 @@ public class SparkZOrderUDF implements Serializable {
         return udf;
     }
 
+    private UserDefinedFunction timestampNtzToLongUDF() {
+        UserDefinedFunction udf =
+                functions
+                        .udf(
+                                (LocalDateTime value) ->
+                                        value == null ? null : 
value.toEpochSecond(ZoneOffset.UTC),
+                                DataTypes.LongType)
+                        .withName("TIMESTAMP_NTZ_LONG");
+
+        return udf;
+    }
+
     private UserDefinedFunction longToOrderedBytesUDF() {
         int position = inputCol;
         UserDefinedFunction udf =
@@ -347,6 +362,8 @@ public class SparkZOrderUDF implements Serializable {
             return booleanToOrderedBytesUDF().apply(column);
         } else if (type instanceof TimestampType) {
             return 
longToOrderedBytesUDF().apply(column.cast(DataTypes.LongType));
+        } else if (type instanceof TimestampNTZType) {
+            return 
longToOrderedBytesUDF().apply(timestampNtzToLongUDF().apply(column));
         } else if (type instanceof DateType) {
             return 
longToOrderedBytesUDF().apply(column.cast(DataTypes.LongType));
         } else {
diff --git 
a/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkHilbertUDFTest.java
 
b/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkHilbertUDFTest.java
index d96b68388d..abb24eb731 100644
--- 
a/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkHilbertUDFTest.java
+++ 
b/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkHilbertUDFTest.java
@@ -28,6 +28,8 @@ import org.apache.spark.sql.types.StructField;
 import org.apache.spark.sql.types.StructType;
 import org.junit.jupiter.api.Test;
 
+import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -85,4 +87,59 @@ public class SparkHilbertUDFTest {
             SparkSession.clearDefaultSession();
         }
     }
+
+    @Test
+    void testTimestampNtzColumnIsSupported() {
+        SparkSession spark =
+                SparkSession.builder()
+                        .master("local[1]")
+                        .appName("spark-hilbert-udf-ntz-test")
+                        .config("spark.ui.enabled", "false")
+                        .getOrCreate();
+        try {
+            StructType schema =
+                    new StructType(
+                            new StructField[] {
+                                new StructField(
+                                        "a", DataTypes.TimestampNTZType, true, 
Metadata.empty())
+                            });
+            Dataset<Row> df =
+                    spark.createDataFrame(
+                            Arrays.asList(
+                                    RowFactory.create(LocalDateTime.of(2024, 
1, 1, 0, 0, 0)),
+                                    RowFactory.create(LocalDateTime.of(2024, 
6, 1, 12, 0, 0)),
+                                    RowFactory.create(LocalDateTime.of(2025, 
1, 1, 0, 0, 0)),
+                                    RowFactory.create((LocalDateTime) null)),
+                            schema);
+
+            SparkHilbertUDF udf = new SparkHilbertUDF();
+            // A paimon TIMESTAMP column reads back as timestamp_ntz unless 
the legacy mapping is
+            // on, and the dispatch used to reject it, so ordering by it threw 
"the type is
+            // unsupported" instead of clustering.
+            List<Row> rows =
+                    df.select(
+                                    df.col("a"),
+                                    udf.sortedLexicographically(
+                                                    df.col("a"), 
DataTypes.TimestampNTZType)
+                                            .as("hilbert"))
+                            .orderBy(df.col("a").asc_nulls_first())
+                            .collectAsList();
+
+            assertThat(rows).hasSize(4);
+            assertThat(rows.get(0).isNullAt(0)).isTrue();
+            assertThat(rows.get(0).getLong(1)).isEqualTo(Long.MAX_VALUE);
+
+            List<Long> nonNull = new ArrayList<>();
+            for (Row row : rows.subList(1, rows.size())) {
+                nonNull.add(row.getLong(1));
+            }
+            for (int i = 1; i < nonNull.size(); i++) {
+                assertThat(nonNull.get(i)).isGreaterThan(nonNull.get(i - 1));
+            }
+        } finally {
+            spark.stop();
+            SparkSession.clearActiveSession();
+            SparkSession.clearDefaultSession();
+        }
+    }
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkZOrderUDFTest.java
 
b/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkZOrderUDFTest.java
index ef07b0f0cb..7a091f8b63 100644
--- 
a/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkZOrderUDFTest.java
+++ 
b/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/sort/SparkZOrderUDFTest.java
@@ -29,6 +29,8 @@ import org.apache.spark.sql.types.StructField;
 import org.apache.spark.sql.types.StructType;
 import org.junit.jupiter.api.Test;
 
+import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -91,4 +93,63 @@ public class SparkZOrderUDFTest {
             SparkSession.clearDefaultSession();
         }
     }
+
+    @Test
+    void testTimestampNtzColumnIsSupported() {
+        SparkSession spark =
+                SparkSession.builder()
+                        .master("local[1]")
+                        .appName("spark-zorder-udf-ntz-test")
+                        .config("spark.ui.enabled", "false")
+                        .getOrCreate();
+        try {
+            StructType schema =
+                    new StructType(
+                            new StructField[] {
+                                new StructField(
+                                        "a", DataTypes.TimestampNTZType, true, 
Metadata.empty())
+                            });
+            Dataset<Row> df =
+                    spark.createDataFrame(
+                            Arrays.asList(
+                                    RowFactory.create(LocalDateTime.of(2024, 
1, 1, 0, 0, 0)),
+                                    RowFactory.create(LocalDateTime.of(2024, 
6, 1, 12, 0, 0)),
+                                    RowFactory.create(LocalDateTime.of(2025, 
1, 1, 0, 0, 0)),
+                                    RowFactory.create((LocalDateTime) null)),
+                            schema);
+
+            SparkZOrderUDF udf = new SparkZOrderUDF(1, 8, Integer.MAX_VALUE);
+            // A paimon TIMESTAMP column reads back as timestamp_ntz unless 
the legacy mapping is
+            // on, and the dispatch used to reject it, so ordering by it threw 
"the type is
+            // unsupported" instead of clustering.
+            List<Row> rows =
+                    df.select(
+                                    df.col("a"),
+                                    functions
+                                            .hex(
+                                                    
udf.sortedLexicographically(
+                                                            df.col("a"),
+                                                            
DataTypes.TimestampNTZType))
+                                            .as("zvalue"))
+                            .orderBy(df.col("a").asc_nulls_first())
+                            .collectAsList();
+
+            assertThat(rows).hasSize(4);
+            assertThat(rows.get(0).isNullAt(0)).isTrue();
+            assertThat(rows.get(0).getString(1)).isEqualTo("0000000000000000");
+
+            // the z-value of a non-null timestamp has to rise with the 
timestamp itself
+            List<String> nonNull = new ArrayList<>();
+            for (Row row : rows.subList(1, rows.size())) {
+                nonNull.add(row.getString(1));
+            }
+            for (int i = 1; i < nonNull.size(); i++) {
+                assertThat(nonNull.get(i)).isGreaterThan(nonNull.get(i - 1));
+            }
+        } finally {
+            spark.stop();
+            SparkSession.clearActiveSession();
+            SparkSession.clearDefaultSession();
+        }
+    }
 }

Reply via email to