149149from pyiceberg .table .name_mapping import NameMapping , apply_name_mapping
150150from pyiceberg .table .puffin import PuffinFile
151151from pyiceberg .transforms import IdentityTransform , TruncateTransform
152- from pyiceberg .typedef import EMPTY_DICT , Properties , Record , TableVersion
152+ from pyiceberg .typedef import EMPTY_DICT , ArrowStreamExportable , Properties , Record , TableVersion
153153from pyiceberg .types import (
154154 BinaryType ,
155155 BooleanType ,
@@ -2680,30 +2680,45 @@ def bin_pack_arrow_table(tbl: pa.Table, target_file_size: int) -> Iterator[list[
26802680 """Bin-pack ``tbl`` into groups of RecordBatches, each ~``target_file_size``.
26812681
26822682 Note:
2683- ``target_file_size`` is measured in **uncompressed in-memory** Arrow bytes
2684- (``Table.nbytes`` / ``RecordBatch.nbytes``), not compressed on-disk Parquet
2685- bytes. The resulting Parquet file after compression (zstd by default,
2686- plus dictionary/RLE encoding) is typically 3-10× smaller than
2687- ``target_file_size``. This is a coarse proxy for the spec-defined
2683+ ``target_file_size`` is measured in **uncompressed in-memory** Arrow
2684+ bytes, not compressed on-disk Parquet bytes. The size estimate uses
2685+ ``nbytes`` when available and falls back to referenced buffer size for
2686+ Arrow view types that do not support ``nbytes``. The resulting Parquet
2687+ file after compression (zstd by default, plus dictionary/RLE encoding)
2688+ is typically 3-10× smaller than ``target_file_size``. This is a coarse
2689+ proxy for the spec-defined
26882690 ``write.target-file-size-bytes`` and will be tightened to true on-disk
26892691 bytes once the writer is switched to a rolling-``ParquetWriter`` with
26902692 ``OutputStream.tell()`` (#2998).
26912693 """
26922694 from pyiceberg .utils .bin_packing import PackingIterator
26932695
2694- avg_row_size_bytes = tbl . nbytes / tbl .num_rows
2696+ avg_row_size_bytes = _arrow_data_size ( tbl ) / tbl .num_rows
26952697 target_rows_per_file = max (1 , int (target_file_size / avg_row_size_bytes ))
26962698 batches = tbl .to_batches (max_chunksize = target_rows_per_file )
26972699 bin_packed_record_batches = PackingIterator (
26982700 items = batches ,
26992701 target_weight = target_file_size ,
27002702 lookback = len (batches ), # ignore lookback
2701- weight_func = lambda x : x . nbytes ,
2703+ weight_func = _arrow_data_size ,
27022704 largest_bin_first = False ,
27032705 )
27042706 return bin_packed_record_batches
27052707
27062708
2709+ def _arrow_data_size (data : pa .Table | pa .RecordBatch ) -> int :
2710+ """Estimate Arrow data size for writer bin-packing.
2711+
2712+ ``nbytes`` is the better logical-size estimate, but PyArrow can raise for
2713+ view types such as ``string_view`` exported by libraries like Polars. Fall
2714+ back to total referenced buffer size so those streams can still be written.
2715+ """
2716+ try :
2717+ return data .nbytes
2718+ except pyarrow .lib .ArrowTypeError :
2719+ return data .get_total_buffer_size ()
2720+
2721+
27072722def bin_pack_record_batches (batches : Iterable [pa .RecordBatch ], target_file_size : int ) -> Iterator [list [pa .RecordBatch ]]:
27082723 """Microbatch a single-pass stream of RecordBatches into target-sized groups.
27092724
@@ -2719,9 +2734,11 @@ def bin_pack_record_batches(batches: Iterable[pa.RecordBatch], target_file_size:
27192734
27202735 Note:
27212736 ``target_file_size`` is measured in **uncompressed in-memory** Arrow
2722- bytes (``RecordBatch.nbytes``), not compressed on-disk Parquet bytes.
2723- The resulting Parquet file after compression is typically 3-10×
2724- smaller than ``target_file_size``. Matches the existing
2737+ bytes, not compressed on-disk Parquet bytes. The size estimate uses
2738+ ``nbytes`` when available and falls back to referenced buffer size for
2739+ Arrow view types that do not support ``nbytes``. The resulting Parquet
2740+ file after compression is typically 3-10× smaller than
2741+ ``target_file_size``. Matches the existing
27252742 :func:`bin_pack_arrow_table` semantics; both will be tightened to true
27262743 on-disk bytes once the writer is switched to a rolling-
27272744 ``ParquetWriter`` with ``OutputStream.tell()`` (#2998).
@@ -2730,7 +2747,7 @@ def bin_pack_record_batches(batches: Iterable[pa.RecordBatch], target_file_size:
27302747 buffer_bytes = 0
27312748 for batch in batches :
27322749 buffer .append (batch )
2733- buffer_bytes += batch . nbytes
2750+ buffer_bytes += _arrow_data_size ( batch )
27342751 if buffer_bytes >= target_file_size :
27352752 yield buffer
27362753 buffer = []
@@ -3033,3 +3050,23 @@ def _get_field_from_arrow_table(arrow_table: pa.Table, field_path: str) -> pa.Ar
30333050 field_array = arrow_table [path_parts [0 ]]
30343051 # Navigate into the struct using the remaining path parts
30353052 return pc .struct_field (field_array , path_parts [1 :])
3053+
3054+
3055+ def _coerce_arrow_input (df : pa .Table | pa .RecordBatchReader | ArrowStreamExportable ) -> pa .Table | pa .RecordBatchReader :
3056+ """Normalize Arrow write input to a pa.Table or pa.RecordBatchReader.
3057+
3058+ Native pyarrow inputs pass through unchanged; any object implementing the
3059+ Arrow PyCapsule stream interface (``__arrow_c_stream__``) is imported as a
3060+ streaming RecordBatchReader.
3061+ """
3062+ if isinstance (df , (pa .Table , pa .RecordBatchReader )):
3063+ return df
3064+
3065+ # Any object implementing the Arrow PyCapsule stream interface.
3066+ if hasattr (df , "__arrow_c_stream__" ):
3067+ return pa .RecordBatchReader .from_stream (df )
3068+
3069+ raise ValueError (
3070+ f"Expected pa.Table, pa.RecordBatchReader, or an object implementing the "
3071+ f"Arrow PyCapsule interface (__arrow_c_stream__), got: { df !r} "
3072+ )
0 commit comments