Skip to content

Commit ccefb86

Browse files
authored
fix: pad sub-microsecond digits when parsing nanosecond (#3614)
timestamp_to_nanos and timestamptz_to_nanos read the sub-microsecond digits of the fraction (regex group 3 of ISO_TIMESTAMP_NANO / ISO_TIMESTAMPTZ_NANO) as a bare integer. For a 7- or 8-digit fraction that group is 1 or 2 digits and represents positions 7-8 of the fraction, so it must be right-padded to 3 digits before being interpreted as nanoseconds. Without padding, .1234567 was parsed as +7 ns instead of +700 ns, silently corrupting the value. Right-pad the group to 3 digits with ljust so all fraction widths round-trip correctly, and add 7- and 8-digit cases to the tests, which previously only covered 6- and 9-digit fractions. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent 800b56a commit ccefb86

2 files changed

Lines changed: 10 additions & 2 deletions

File tree

‎pyiceberg/utils/datetime.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ def timestamp_to_nanos(timestamp_str: str) -> int:
121121
if match := ISO_TIMESTAMP_NANO.fullmatch(timestamp_str):
122122
# Python datetime does not have native nanoseconds support
123123
# Hence we need to extract nanoseconds timestamp manually
124-
ns_str = match.group(3) or "0"
124+
# group(3) holds the sub-microsecond digits (fraction positions 7-9), so
125+
# right-pad to 3 digits before reading them as nanoseconds (e.g. "7" -> 700).
126+
ns_str = (match.group(3) or "0").ljust(3, "0")
125127
ms_str = match.group(2) if match.group(2) else ""
126128
timestamp_str_without_ns_str = match.group(1) + ms_str
127129
return datetime_to_nanos(datetime.fromisoformat(timestamp_str_without_ns_str)) + int(ns_str)
@@ -136,7 +138,9 @@ def timestamptz_to_nanos(timestamptz_str: str) -> int:
136138
if match := ISO_TIMESTAMPTZ_NANO.fullmatch(timestamptz_str):
137139
# Python datetime does not have native nanoseconds support
138140
# Hence we need to extract nanoseconds timestamp manually
139-
ns_str = match.group(3) or "0"
141+
# group(3) holds the sub-microsecond digits (fraction positions 7-9), so
142+
# right-pad to 3 digits before reading them as nanoseconds (e.g. "7" -> 700).
143+
ns_str = (match.group(3) or "0").ljust(3, "0")
140144
ms_str = match.group(2) if match.group(2) else ""
141145
timestamptz_str_without_ns_str = match.group(1) + ms_str + match.group(4)
142146
return datetime_to_nanos(datetime.fromisoformat(timestamptz_str_without_ns_str)) + int(ns_str)

‎tests/utils/test_datetime.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ def test_datetime_to_nanos(datetime_: datetime, nanos: int) -> None:
111111
[
112112
("1970-01-01T00:00:00", 0),
113113
("2025-02-23T20:21:44.375612", 1740342104375612000),
114+
("2025-02-23T20:21:44.3756127", 1740342104375612700),
115+
("2025-02-23T20:21:44.37561278", 1740342104375612780),
114116
("2025-02-23T20:21:44.375612001", 1740342104375612001),
115117
],
116118
)
@@ -128,6 +130,8 @@ def test_timestamp_to_nanos_unexpected_zone_offset() -> None:
128130
[
129131
("1970-01-01T00:00:00+00:00", 0),
130132
("2025-02-23T16:21:44.375612-04:00", 1740342104375612000),
133+
("2025-02-23T16:21:44.3756127-04:00", 1740342104375612700),
134+
("2025-02-23T16:21:44.37561278-04:00", 1740342104375612780),
131135
("2025-02-23T16:21:44.375612001-04:00", 1740342104375612001),
132136
],
133137
)

0 commit comments

Comments
 (0)