You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by gi...@apache.org on 2026/09/25 21:39:19 UTC

(iceberg-rust) branch main updated: fix(spec): format pre-epoch timestamptz values (#2892)

This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 9a2111a91 fix(spec): format pre-epoch timestamptz values (#2892)
9a2111a91 is described below

commit 9a2111a914e9b17fdc039700186056ec14fae606
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Fri Sep 25 21:32:09 2026 +0000

    fix(spec): format pre-epoch timestamptz values (#2892)
    
    * fix(spec): format pre-epoch timestamptz values
    
    * fix(spec): strengthen pre-epoch timestamptz regression
    
    * style: format temporal conversion
    
    * test(spec): match chrono fractional timestamp display
    
    * test(spec): match trimmed timestamp fraction
---
 crates/iceberg/src/spec/values/temporal.rs | 12 +++++------
 crates/iceberg/src/spec/values/tests.rs    | 32 ++++++++++++++++++++++++++++++
 2 files changed, 38 insertions(+), 6 deletions(-)

diff --git a/crates/iceberg/src/spec/values/temporal.rs b/crates/iceberg/src/spec/values/temporal.rs
index 80aadeb65..76b2c897e 100644
--- a/crates/iceberg/src/spec/values/temporal.rs
+++ b/crates/iceberg/src/spec/values/temporal.rs
@@ -98,15 +98,15 @@ pub(crate) mod timestamptz {
     }
 
     pub(crate) fn microseconds_to_datetimetz(micros: i64) -> DateTime<Utc> {
-        let (secs, rem) = (micros / 1_000_000, micros % 1_000_000);
-
-        DateTime::from_timestamp(secs, rem as u32 * 1_000).unwrap()
+        // Returns None only outside chrono's range (~year -262144 to +262142);
+        // unreachable for any realistic Iceberg timestamp.
+        DateTime::from_timestamp_micros(micros).expect(
+            "timestamp micros outside chrono's representable range (~year -262144..+262142)",
+        )
     }
 
     pub(crate) fn nanoseconds_to_datetimetz(nanos: i64) -> DateTime<Utc> {
-        let (secs, rem) = (nanos / 1_000_000_000, nanos % 1_000_000_000);
-
-        DateTime::from_timestamp(secs, rem as u32).unwrap()
+        DateTime::from_timestamp_nanos(nanos)
     }
 
     /// Nanoseconds since the Unix epoch, or `None` if outside the representable `i64` range
diff --git a/crates/iceberg/src/spec/values/tests.rs b/crates/iceberg/src/spec/values/tests.rs
index 97b2f6b32..ff51f318e 100644
--- a/crates/iceberg/src/spec/values/tests.rs
+++ b/crates/iceberg/src/spec/values/tests.rs
@@ -1124,6 +1124,38 @@ fn test_parse_timestamptz() {
     );
 }
 
+#[test]
+fn test_pre_epoch_timestamptz_formatting() {
+    assert_eq!(
+        Datum::timestamptz_micros(-1).to_string(),
+        "1969-12-31 23:59:59.999999 UTC"
+    );
+    assert_eq!(
+        Datum::timestamptz_nanos(-1).to_string(),
+        "1969-12-31 23:59:59.999999999 UTC"
+    );
+    assert_eq!(
+        Datum::timestamptz_micros(-1_500_000).to_string(),
+        "1969-12-31 23:59:58.500 UTC"
+    );
+
+    check_json_serde(
+        r#""1969-12-31T23:59:59.999999+00:00""#,
+        Literal::long(-1),
+        &Primitive(PrimitiveType::Timestamptz),
+    );
+    check_json_serde(
+        r#""1969-12-31T23:59:58.500+00:00""#,
+        Literal::long(-1_500_000),
+        &Primitive(PrimitiveType::Timestamptz),
+    );
+    check_json_serde(
+        r#""1969-12-31T23:59:59.999999999+00:00""#,
+        Literal::long(-1),
+        &Primitive(PrimitiveType::TimestamptzNs),
+    );
+}
+
 #[test]
 fn test_datum_ser_deser() {
     let test_fn = |datum: Datum| {