Skip to content

Commit c887117

Browse files
committed
Deprecate 'downcast-ns-timestamp-to-us-on-write' config key in favor of 'downcast-ns-timestamp-to-us'
The config key 'downcast-ns-timestamp-to-us-on-write' says 'on-write' but is also used in the read path (ArrowScan). Rename the canonical key to 'downcast-ns-timestamp-to-us' which accurately describes the behavior regardless of direction. The old key still works but emits a DeprecationWarning. The new key takes precedence when both are set.
1 parent 9d36e23 commit c887117

8 files changed

Lines changed: 149 additions & 26 deletions

File tree

mkdocs/docs/configuration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,4 +905,7 @@ Previous versions of Java (`<1.4.0`) implementations incorrectly assume the opti
905905

906906
## Nanoseconds Support
907907

908-
PyIceberg currently only supports upto microsecond precision in its TimestampType. PyArrow timestamp types in 's' and 'ms' will be upcast automatically to 'us' precision timestamps on write. Timestamps in 'ns' precision can also be downcast automatically on write if desired. This can be configured by setting the `downcast-ns-timestamp-to-us-on-write` property as "True" in the configuration file, or by setting the `PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE` environment variable. Refer to the [nanoseconds timestamp proposal document](https://docs.google.com/document/d/1bE1DcEGNzZAMiVJSZ0X1wElKLNkT9kRkk0hDlfkXzvU/edit#heading=h.ibflcctc9i1d) for more details on the long term roadmap for nanoseconds support
908+
PyIceberg currently only supports upto microsecond precision in its TimestampType. PyArrow timestamp types in 's' and 'ms' will be upcast automatically to 'us' precision timestamps on write. Timestamps in 'ns' precision can also be downcast automatically when desired. This can be configured by setting the `downcast-ns-timestamp-to-us` property as "True" in the configuration file, or by setting the `PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US` environment variable. Refer to the [nanoseconds timestamp proposal document](https://docs.google.com/document/d/1bE1DcEGNzZAMiVJSZ0X1wElKLNkT9kRkk0hDlfkXzvU/edit#heading=h.ibflcctc9i1d) for more details on the long term roadmap for nanoseconds support.
909+
910+
!!! note "Deprecated config key"
911+
The previous config key `downcast-ns-timestamp-to-us-on-write` (env: `PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE`) is deprecated. It still works but will emit a deprecation warning. Migrate to `downcast-ns-timestamp-to-us` (env: `PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US`).

pyiceberg/catalog/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@
4646
from pyiceberg.schema import Schema
4747
from pyiceberg.serializers import ToOutputFile
4848
from pyiceberg.table import (
49-
DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE,
5049
CommitTableResponse,
5150
CreateTableTransaction,
5251
StagedTable,
5352
Table,
5453
TableProperties,
54+
_get_downcast_ns_timestamp_to_us,
5555
)
5656
from pyiceberg.table.locations import load_location_provider
5757
from pyiceberg.table.metadata import TableMetadata, TableMetadataV1, new_table_metadata
@@ -842,7 +842,7 @@ def _convert_schema_if_needed(
842842

843843
from pyiceberg.io.pyarrow import _ConvertToIcebergWithoutIDs, visit_pyarrow
844844

845-
downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) or False
845+
downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
846846
if isinstance(schema, pa.Schema):
847847
schema: Schema = visit_pyarrow( # type: ignore
848848
schema,

pyiceberg/io/pyarrow.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
visit,
144144
visit_with_partner,
145145
)
146-
from pyiceberg.table import DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE, TableProperties
146+
from pyiceberg.table import TableProperties, _get_downcast_ns_timestamp_to_us
147147
from pyiceberg.table.deletion_vector import deletion_vectors_from_puffin_file
148148
from pyiceberg.table.locations import load_location_provider
149149
from pyiceberg.table.metadata import TableMetadata
@@ -180,7 +180,6 @@
180180
strtobool,
181181
)
182182
from pyiceberg.utils.concurrent import ExecutorFactory
183-
from pyiceberg.utils.config import Config
184183
from pyiceberg.utils.datetime import millis_to_datetime
185184
from pyiceberg.utils.decimal import unscaled_to_decimal
186185
from pyiceberg.utils.properties import get_first_property_value, property_as_bool, property_as_int
@@ -1467,8 +1466,8 @@ def primitive(self, primitive: pa.DataType) -> PrimitiveType:
14671466
else:
14681467
raise TypeError(
14691468
"Iceberg does not yet support 'ns' timestamp precision. "
1470-
"Use 'downcast-ns-timestamp-to-us-on-write' configuration property to automatically "
1471-
"downcast 'ns' to 'us' on write.",
1469+
"Use 'downcast-ns-timestamp-to-us' configuration property to automatically "
1470+
"downcast 'ns' to 'us'.",
14721471
)
14731472
else:
14741473
raise TypeError(f"Unsupported precision for timestamp type: {primitive.unit}")
@@ -1763,7 +1762,7 @@ def __init__(
17631762
self._bound_row_filter = bind(table_metadata.schema(), row_filter, case_sensitive=case_sensitive)
17641763
self._case_sensitive = case_sensitive
17651764
self._limit = limit
1766-
self._downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE)
1765+
self._downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
17671766
self._dictionary_columns = dictionary_columns
17681767

17691768
@property
@@ -2615,7 +2614,7 @@ def data_file_statistics_from_parquet_metadata(
26152614

26162615

26172616
def write_file(io: FileIO, table_metadata: TableMetadata, tasks: Iterator[WriteTask]) -> Iterator[DataFile]:
2618-
from pyiceberg.table import DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE, TableProperties
2617+
from pyiceberg.table import TableProperties
26192618

26202619
parquet_writer_kwargs = _get_parquet_writer_kwargs(table_metadata.properties)
26212620
row_group_size = property_as_int(
@@ -2634,7 +2633,7 @@ def write_parquet(task: WriteTask) -> DataFile:
26342633
else:
26352634
file_schema = table_schema
26362635

2637-
downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) or False
2636+
downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
26382637
batches = [
26392638
_to_requested_schema(
26402639
requested_schema=file_schema,
@@ -2887,7 +2886,7 @@ def _dataframe_to_data_files(
28872886
Returns:
28882887
An iterable that supplies datafiles that represent the input data.
28892888
"""
2890-
from pyiceberg.table import DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE, TableProperties, WriteTask
2889+
from pyiceberg.table import TableProperties, WriteTask
28912890

28922891
counter = counter or itertools.count(0)
28932892
write_uuid = write_uuid or uuid.uuid4()
@@ -2897,7 +2896,7 @@ def _dataframe_to_data_files(
28972896
default=TableProperties.WRITE_TARGET_FILE_SIZE_BYTES_DEFAULT,
28982897
)
28992898
name_mapping = table_metadata.schema().name_mapping
2900-
downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) or False
2899+
downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
29012900
task_schema = pyarrow_to_schema(
29022901
df.schema,
29032902
name_mapping=name_mapping,

pyiceberg/table/__init__.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,39 @@
116116
from pyiceberg.catalog.rest.scan_planning import RESTContentFile, RESTDeleteFile, RESTFileScanTask
117117

118118
ALWAYS_TRUE = AlwaysTrue()
119+
DOWNCAST_NS_TIMESTAMP_TO_US = "downcast-ns-timestamp-to-us"
120+
# Deprecated: use DOWNCAST_NS_TIMESTAMP_TO_US. The old key said "on-write" but the
121+
# config also controls read-path downcasting, so the name was misleading.
119122
DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE = "downcast-ns-timestamp-to-us-on-write"
120123

121124

125+
def _get_downcast_ns_timestamp_to_us() -> bool:
126+
"""Return the effective value of the downcast-ns-timestamp-to-us config.
127+
128+
Checks the new ``downcast-ns-timestamp-to-us`` key first. If not set, falls
129+
back to the deprecated ``downcast-ns-timestamp-to-us-on-write`` key and
130+
emits a :class:`DeprecationWarning` so callers can migrate their config.
131+
"""
132+
config = Config()
133+
134+
value = config.get_bool(DOWNCAST_NS_TIMESTAMP_TO_US)
135+
if value is not None:
136+
return value
137+
138+
legacy = config.get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE)
139+
if legacy is not None:
140+
warnings.warn(
141+
f"Config key '{DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE}' (env: PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) "
142+
f"is deprecated. Use '{DOWNCAST_NS_TIMESTAMP_TO_US}' "
143+
"(env: PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US) instead.",
144+
DeprecationWarning,
145+
stacklevel=2,
146+
)
147+
return legacy
148+
149+
return False
150+
151+
122152
@dataclass()
123153
class UpsertResult:
124154
"""Summary the upsert operation."""
@@ -517,7 +547,7 @@ def append(
517547
if not isinstance(df, (pa.Table, pa.RecordBatchReader)):
518548
raise ValueError(f"Expected pa.Table or pa.RecordBatchReader, got: {df}")
519549

520-
downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) or False
550+
downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
521551
_check_pyarrow_schema_compatible(
522552
self.table_metadata.schema(),
523553
provided_schema=df.schema,
@@ -573,7 +603,7 @@ def dynamic_partition_overwrite(
573603
f"in the latest partition spec: {field}"
574604
)
575605

576-
downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) or False
606+
downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
577607
_check_pyarrow_schema_compatible(
578608
self.table_metadata.schema(),
579609
provided_schema=df.schema,
@@ -674,7 +704,7 @@ def overwrite(
674704
if not isinstance(df, (pa.Table, pa.RecordBatchReader)):
675705
raise ValueError(f"Expected pa.Table or pa.RecordBatchReader, got: {df}")
676706

677-
downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) or False
707+
downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
678708
_check_pyarrow_schema_compatible(
679709
self.table_metadata.schema(),
680710
provided_schema=df.schema,
@@ -873,7 +903,7 @@ def upsert(
873903

874904
from pyiceberg.io.pyarrow import _check_pyarrow_schema_compatible
875905

876-
downcast_ns_timestamp_to_us = Config().get_bool(DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE) or False
906+
downcast_ns_timestamp_to_us = _get_downcast_ns_timestamp_to_us()
877907
_check_pyarrow_schema_compatible(
878908
self.table_metadata.schema(),
879909
provided_schema=df.schema,
@@ -2471,8 +2501,9 @@ def plan_files(self) -> Iterable[FileScanTask]:
24712501
options=self.options,
24722502
).plan_files(
24732503
manifests=manifests,
2474-
manifest_entry_filter=lambda manifest_entry: manifest_entry.snapshot_id in append_snapshot_ids
2475-
and manifest_entry.status == ManifestEntryStatus.ADDED,
2504+
manifest_entry_filter=lambda manifest_entry: (
2505+
manifest_entry.snapshot_id in append_snapshot_ids and manifest_entry.status == ManifestEntryStatus.ADDED
2506+
),
24762507
)
24772508

24782509
def to_arrow(self) -> pa.Table:

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ filterwarnings = [
180180
"ignore:As the c extension couldn't be imported:RuntimeWarning:google_crc32c",
181181
# Ignore Spark 4.0.1 pandas conversion warning under pandas 3.0
182182
"ignore:The copy keyword is deprecated and will be removed in a future version.*",
183+
# Deprecated config key migration (backwards compat)
184+
"ignore:Config key 'downcast-ns-timestamp-to-us-on-write'.*:DeprecationWarning",
183185
]
184186

185187
[tool.mypy]

tests/integration/test_add_files.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ def test_add_files_with_timestamp_tz_ns_fails(session_catalog: Catalog, format_v
732732
],
733733
schema=nanoseconds_schema,
734734
)
735-
mocker.patch.dict(os.environ, values={"PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE": "True"})
735+
mocker.patch.dict(os.environ, values={"PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US": "True"})
736736

737737
identifier = f"default.timestamptz_ns_added{format_version}"
738738
tbl = _create_table(session_catalog, identifier, format_version, schema=nanoseconds_schema_iceberg)
@@ -754,8 +754,8 @@ def test_add_files_with_timestamp_tz_ns_fails(session_catalog: Catalog, format_v
754754
exception_cause = exc_info.value.__cause__
755755
assert isinstance(exception_cause, TypeError)
756756
assert (
757-
"Iceberg does not yet support 'ns' timestamp precision. Use 'downcast-ns-timestamp-to-us-on-write' "
758-
"configuration property to automatically downcast 'ns' to 'us' on write." in exception_cause.args[0]
757+
"Iceberg does not yet support 'ns' timestamp precision. Use 'downcast-ns-timestamp-to-us' "
758+
"configuration property to automatically downcast 'ns' to 'us'." in exception_cause.args[0]
759759
)
760760

761761

tests/integration/test_writes/test_writes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1536,7 +1536,7 @@ def test_write_all_timestamp_precision(
15361536
arrow_table_schema_with_all_microseconds_timestamp_precisions: pa.Schema,
15371537
) -> None:
15381538
identifier = "default.table_all_timestamp_precision"
1539-
mocker.patch.dict(os.environ, values={"PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE": "True"})
1539+
mocker.patch.dict(os.environ, values={"PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US": "True"})
15401540

15411541
tbl = _create_table(
15421542
session_catalog,

tests/io/test_pyarrow_visitor.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,101 @@ def test_pyarrow_timestamp_invalid_units() -> None:
191191
with pytest.raises(
192192
TypeError,
193193
match=re.escape(
194-
"Iceberg does not yet support 'ns' timestamp precision. Use 'downcast-ns-timestamp-to-us-on-write' "
195-
"configuration property to automatically downcast 'ns' to 'us' on write."
194+
"Iceberg does not yet support 'ns' timestamp precision. Use 'downcast-ns-timestamp-to-us' "
195+
"configuration property to automatically downcast 'ns' to 'us'."
196196
),
197197
):
198198
visit_pyarrow(pyarrow_type, _ConvertToIceberg())
199199

200200

201+
def test_downcast_ns_timestamp_legacy_env_var_is_backwards_compat() -> None:
202+
"""The deprecated PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE env var still activates downcasting."""
203+
import os
204+
import warnings
205+
206+
from pyiceberg.table import _get_downcast_ns_timestamp_to_us
207+
208+
env_key = "PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE"
209+
old_value = os.environ.get(env_key)
210+
try:
211+
os.environ[env_key] = "True"
212+
with warnings.catch_warnings(record=True) as caught:
213+
warnings.simplefilter("always")
214+
result = _get_downcast_ns_timestamp_to_us()
215+
assert result is True, "Legacy env var should still activate downcasting"
216+
deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)]
217+
assert len(deprecation_warnings) == 1
218+
assert "downcast-ns-timestamp-to-us-on-write" in str(deprecation_warnings[0].message)
219+
assert "downcast-ns-timestamp-to-us" in str(deprecation_warnings[0].message)
220+
finally:
221+
if old_value is None:
222+
os.environ.pop(env_key, None)
223+
else:
224+
os.environ[env_key] = old_value
225+
226+
227+
def test_downcast_ns_timestamp_new_env_var_takes_precedence() -> None:
228+
"""The new PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US env var works and emits no deprecation warning."""
229+
import os
230+
import warnings
231+
232+
from pyiceberg.table import _get_downcast_ns_timestamp_to_us
233+
234+
new_key = "PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US"
235+
old_key = "PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE"
236+
old_new = os.environ.get(new_key)
237+
old_legacy = os.environ.get(old_key)
238+
try:
239+
os.environ[new_key] = "True"
240+
os.environ.pop(old_key, None)
241+
with warnings.catch_warnings(record=True) as caught:
242+
warnings.simplefilter("always")
243+
result = _get_downcast_ns_timestamp_to_us()
244+
assert result is True
245+
deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)]
246+
assert len(deprecation_warnings) == 0, "New key must not emit a deprecation warning"
247+
finally:
248+
if old_new is None:
249+
os.environ.pop(new_key, None)
250+
else:
251+
os.environ[new_key] = old_new
252+
if old_legacy is None:
253+
os.environ.pop(old_key, None)
254+
else:
255+
os.environ[old_key] = old_legacy
256+
257+
258+
def test_downcast_ns_timestamp_new_key_overrides_legacy_key() -> None:
259+
"""When both keys are set, the new key wins and no deprecation warning is emitted."""
260+
import os
261+
import warnings
262+
263+
from pyiceberg.table import _get_downcast_ns_timestamp_to_us
264+
265+
new_key = "PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US"
266+
old_key = "PYICEBERG_DOWNCAST_NS_TIMESTAMP_TO_US_ON_WRITE"
267+
old_new = os.environ.get(new_key)
268+
old_legacy = os.environ.get(old_key)
269+
try:
270+
os.environ[new_key] = "False"
271+
os.environ[old_key] = "True" # legacy says True, but new key says False
272+
with warnings.catch_warnings(record=True) as caught:
273+
warnings.simplefilter("always")
274+
result = _get_downcast_ns_timestamp_to_us()
275+
assert result is False, "New key must win over legacy key"
276+
deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)]
277+
assert len(deprecation_warnings) == 0, "New key present: no deprecation warning expected"
278+
finally:
279+
if old_new is None:
280+
os.environ.pop(new_key, None)
281+
else:
282+
os.environ[new_key] = old_new
283+
if old_legacy is None:
284+
os.environ.pop(old_key, None)
285+
else:
286+
os.environ[old_key] = old_legacy
287+
288+
201289
def test_pyarrow_timestamp_tz_to_iceberg() -> None:
202290
pyarrow_type = pa.timestamp(unit="us", tz="UTC")
203291
pyarrow_type_zero_offset = pa.timestamp(unit="us", tz="+00:00")
@@ -214,8 +302,8 @@ def test_pyarrow_timestamp_tz_invalid_units() -> None:
214302
with pytest.raises(
215303
TypeError,
216304
match=re.escape(
217-
"Iceberg does not yet support 'ns' timestamp precision. Use 'downcast-ns-timestamp-to-us-on-write' "
218-
"configuration property to automatically downcast 'ns' to 'us' on write."
305+
"Iceberg does not yet support 'ns' timestamp precision. Use 'downcast-ns-timestamp-to-us' "
306+
"configuration property to automatically downcast 'ns' to 'us'."
219307
),
220308
):
221309
visit_pyarrow(pyarrow_type, _ConvertToIceberg())

0 commit comments

Comments
 (0)