150150from pyiceberg .table .name_mapping import NameMapping , apply_name_mapping
151151from pyiceberg .table .puffin import PuffinFile
152152from pyiceberg .transforms import IdentityTransform , TruncateTransform
153- from pyiceberg .typedef import EMPTY_DICT , Properties , Record , TableVersion
153+ from pyiceberg .typedef import EMPTY_DICT , ArrowStreamExportable , Properties , Record , TableVersion
154154from pyiceberg .types import (
155155 BinaryType ,
156156 BooleanType ,
@@ -2690,30 +2690,45 @@ def bin_pack_arrow_table(tbl: pa.Table, target_file_size: int) -> Iterator[list[
26902690 """Bin-pack ``tbl`` into groups of RecordBatches, each ~``target_file_size``.
26912691
26922692 Note:
2693- ``target_file_size`` is measured in **uncompressed in-memory** Arrow bytes
2694- (``Table.nbytes`` / ``RecordBatch.nbytes``), not compressed on-disk Parquet
2695- bytes. The resulting Parquet file after compression (zstd by default,
2696- plus dictionary/RLE encoding) is typically 3-10× smaller than
2697- ``target_file_size``. This is a coarse proxy for the spec-defined
2693+ ``target_file_size`` is measured in **uncompressed in-memory** Arrow
2694+ bytes, not compressed on-disk Parquet bytes. The size estimate uses
2695+ ``nbytes`` when available and falls back to referenced buffer size for
2696+ Arrow view types that do not support ``nbytes``. The resulting Parquet
2697+ file after compression (zstd by default, plus dictionary/RLE encoding)
2698+ is typically 3-10× smaller than ``target_file_size``. This is a coarse
2699+ proxy for the spec-defined
26982700 ``write.target-file-size-bytes`` and will be tightened to true on-disk
26992701 bytes once the writer is switched to a rolling-``ParquetWriter`` with
27002702 ``OutputStream.tell()`` (#2998).
27012703 """
27022704 from pyiceberg .utils .bin_packing import PackingIterator
27032705
2704- avg_row_size_bytes = tbl . nbytes / tbl .num_rows
2706+ avg_row_size_bytes = _arrow_data_size ( tbl ) / tbl .num_rows
27052707 target_rows_per_file = max (1 , int (target_file_size / avg_row_size_bytes ))
27062708 batches = tbl .to_batches (max_chunksize = target_rows_per_file )
27072709 bin_packed_record_batches = PackingIterator (
27082710 items = batches ,
27092711 target_weight = target_file_size ,
27102712 lookback = len (batches ), # ignore lookback
2711- weight_func = lambda x : x . nbytes ,
2713+ weight_func = _arrow_data_size ,
27122714 largest_bin_first = False ,
27132715 )
27142716 return bin_packed_record_batches
27152717
27162718
2719+ def _arrow_data_size (data : pa .Table | pa .RecordBatch ) -> int :
2720+ """Estimate Arrow data size for writer bin-packing.
2721+
2722+ ``nbytes`` is the better logical-size estimate, but PyArrow can raise for
2723+ view types such as ``string_view`` exported by libraries like Polars. Fall
2724+ back to total referenced buffer size so those streams can still be written.
2725+ """
2726+ try :
2727+ return data .nbytes
2728+ except pyarrow .lib .ArrowTypeError :
2729+ return data .get_total_buffer_size ()
2730+
2731+
27172732def bin_pack_record_batches (batches : Iterable [pa .RecordBatch ], target_file_size : int ) -> Iterator [list [pa .RecordBatch ]]:
27182733 """Microbatch a single-pass stream of RecordBatches into target-sized groups.
27192734
@@ -2729,9 +2744,11 @@ def bin_pack_record_batches(batches: Iterable[pa.RecordBatch], target_file_size:
27292744
27302745 Note:
27312746 ``target_file_size`` is measured in **uncompressed in-memory** Arrow
2732- bytes (``RecordBatch.nbytes``), not compressed on-disk Parquet bytes.
2733- The resulting Parquet file after compression is typically 3-10×
2734- smaller than ``target_file_size``. Matches the existing
2747+ bytes, not compressed on-disk Parquet bytes. The size estimate uses
2748+ ``nbytes`` when available and falls back to referenced buffer size for
2749+ Arrow view types that do not support ``nbytes``. The resulting Parquet
2750+ file after compression is typically 3-10× smaller than
2751+ ``target_file_size``. Matches the existing
27352752 :func:`bin_pack_arrow_table` semantics; both will be tightened to true
27362753 on-disk bytes once the writer is switched to a rolling-
27372754 ``ParquetWriter`` with ``OutputStream.tell()`` (#2998).
@@ -2740,7 +2757,7 @@ def bin_pack_record_batches(batches: Iterable[pa.RecordBatch], target_file_size:
27402757 buffer_bytes = 0
27412758 for batch in batches :
27422759 buffer .append (batch )
2743- buffer_bytes += batch . nbytes
2760+ buffer_bytes += _arrow_data_size ( batch )
27442761 if buffer_bytes >= target_file_size :
27452762 yield buffer
27462763 buffer = []
@@ -3043,3 +3060,23 @@ def _get_field_from_arrow_table(arrow_table: pa.Table, field_path: str) -> pa.Ar
30433060 field_array = arrow_table [path_parts [0 ]]
30443061 # Navigate into the struct using the remaining path parts
30453062 return pc .struct_field (field_array , path_parts [1 :])
3063+
3064+
3065+ def _coerce_arrow_input (df : pa .Table | pa .RecordBatchReader | ArrowStreamExportable ) -> pa .Table | pa .RecordBatchReader :
3066+ """Normalize Arrow write input to a pa.Table or pa.RecordBatchReader.
3067+
3068+ Native pyarrow inputs pass through unchanged; any object implementing the
3069+ Arrow PyCapsule stream interface (``__arrow_c_stream__``) is imported as a
3070+ streaming RecordBatchReader.
3071+ """
3072+ if isinstance (df , (pa .Table , pa .RecordBatchReader )):
3073+ return df
3074+
3075+ # Any object implementing the Arrow PyCapsule stream interface.
3076+ if hasattr (df , "__arrow_c_stream__" ):
3077+ return pa .RecordBatchReader .from_stream (df )
3078+
3079+ raise ValueError (
3080+ f"Expected pa.Table, pa.RecordBatchReader, or an object implementing the "
3081+ f"Arrow PyCapsule interface (__arrow_c_stream__), got: { df !r} "
3082+ )
0 commit comments