Skip to content

Commit 743d098

Browse files
authored
feat(spec): read timestamp_ns/timestamptz_ns default values in the Literal JSON codec (#2832)
## Which issue does this PR close? We don't have an issue for this. Deserializing Timestamp NS default values failed. ## What changes are included in this PR? `try_into_json` serialized TimestampNs/TimestamptzNs but `try_from_json` had no matching arms, so a nanosecond-timestamp single value (e.g. a column default) failed to deserialize (fell to the DataInvalid catch-all). Add the read arms mirroring the microsecond ones, plus `datetime_to_nanoseconds` / `datetimetz_to_nanoseconds` helpers. Also adds json_timestamp_ns / json_timestamptz_ns tests. ## Are these changes tested? Yes
1 parent ababe65 commit 743d098

3 files changed

Lines changed: 86 additions & 0 deletions

File tree

crates/iceberg/src/spec/values/literal.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,33 @@ impl Literal {
493493
)),
494494
))))
495495
}
496+
(PrimitiveType::TimestampNs, JsonValue::String(s)) => {
497+
let ndt = NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f")?;
498+
let nanos = timestamp::datetime_to_nanoseconds(&ndt).ok_or_else(|| {
499+
Error::new(
500+
crate::ErrorKind::DataInvalid,
501+
format!(
502+
"Timestamp is outside the representable nanosecond range: {ndt}"
503+
),
504+
)
505+
})?;
506+
Ok(Some(Literal::Primitive(PrimitiveLiteral::Long(nanos))))
507+
}
508+
(PrimitiveType::TimestamptzNs, JsonValue::String(s)) => {
509+
let dt = Utc.from_utc_datetime(&NaiveDateTime::parse_from_str(
510+
&s,
511+
"%Y-%m-%dT%H:%M:%S%.f+00:00",
512+
)?);
513+
let nanos = timestamptz::datetimetz_to_nanoseconds(&dt).ok_or_else(|| {
514+
Error::new(
515+
crate::ErrorKind::DataInvalid,
516+
format!(
517+
"Timestamptz is outside the representable nanosecond range: {dt}"
518+
),
519+
)
520+
})?;
521+
Ok(Some(Literal::Primitive(PrimitiveLiteral::Long(nanos))))
522+
}
496523
(PrimitiveType::String, JsonValue::String(s)) => {
497524
Ok(Some(Literal::Primitive(PrimitiveLiteral::String(s))))
498525
}

crates/iceberg/src/spec/values/temporal.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ pub(crate) mod timestamp {
8282
pub(crate) fn nanoseconds_to_datetime(nanos: i64) -> NaiveDateTime {
8383
DateTime::from_timestamp_nanos(nanos).naive_utc()
8484
}
85+
86+
/// Nanoseconds since the Unix epoch, or `None` if outside the representable `i64` range
87+
/// (roughly the years 1678–2262).
88+
pub(crate) fn datetime_to_nanoseconds(time: &NaiveDateTime) -> Option<i64> {
89+
time.and_utc().timestamp_nanos_opt()
90+
}
8591
}
8692

8793
pub(crate) mod timestamptz {
@@ -102,4 +108,10 @@ pub(crate) mod timestamptz {
102108

103109
DateTime::from_timestamp(secs, rem as u32).unwrap()
104110
}
111+
112+
/// Nanoseconds since the Unix epoch, or `None` if outside the representable `i64` range
113+
/// (roughly the years 1678–2262).
114+
pub(crate) fn datetimetz_to_nanoseconds(time: &DateTime<Utc>) -> Option<i64> {
115+
time.timestamp_nanos_opt()
116+
}
105117
}

crates/iceberg/src/spec/values/tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,53 @@ fn json_timestamptz() {
214214
);
215215
}
216216

217+
#[test]
218+
fn json_timestamp_ns() {
219+
let record = r#""2017-11-16T22:31:08.123456789""#;
220+
221+
check_json_serde(
222+
record,
223+
Literal::Primitive(PrimitiveLiteral::Long(1510871468123456789)),
224+
&Type::Primitive(PrimitiveType::TimestampNs),
225+
);
226+
}
227+
228+
#[test]
229+
fn json_timestamptz_ns() {
230+
let record = r#""2017-11-16T22:31:08.123456789+00:00""#;
231+
232+
check_json_serde(
233+
record,
234+
Literal::Primitive(PrimitiveLiteral::Long(1510871468123456789)),
235+
&Type::Primitive(PrimitiveType::TimestamptzNs),
236+
);
237+
}
238+
239+
#[test]
240+
fn json_timestamptz_ns_rejects_non_utc_offset() {
241+
// Per the spec, timestamptz_ns single-value serialization must use offset "+00:00"; Java's
242+
// SingleValueParser enforces the same (DateTimeUtil.isUTCTimestamptz). A non-UTC offset is not a
243+
// valid encoding and must be rejected, not silently re-based to UTC.
244+
let record = serde_json::Value::String("2017-11-16T22:31:08.123456789+05:00".to_string());
245+
let result = Literal::try_from_json(record, &Type::Primitive(PrimitiveType::TimestamptzNs));
246+
assert!(
247+
result.is_err(),
248+
"non-UTC offset must be rejected for timestamptz_ns, got {result:?}"
249+
);
250+
}
251+
252+
#[test]
253+
fn json_timestamptz_rejects_non_utc_offset() {
254+
// Micros-precision counterpart, mirroring Java's TestSingleValueParser.testInvalidTimestamptz:
255+
// the offset must be "+00:00", so a non-UTC offset is rejected.
256+
let record = serde_json::Value::String("2017-11-16T22:31:08.123456+05:00".to_string());
257+
let result = Literal::try_from_json(record, &Type::Primitive(PrimitiveType::Timestamptz));
258+
assert!(
259+
result.is_err(),
260+
"non-UTC offset must be rejected for timestamptz, got {result:?}"
261+
);
262+
}
263+
217264
#[test]
218265
fn json_string() {
219266
let record = r#""iceberg""#;

0 commit comments

Comments
 (0)