Copilot commented on code in PR #3889:
URL: https://github.com/apache/iceberg-python/pull/3889#discussion_r3902482501


##########
tests/io/test_pyarrow.py:
##########
@@ -5462,3 +5463,89 @@ def 
test_dictionary_columns_produces_dict_encoded_output(tmpdir: str) -> None:
 
     # Values must be identical
     assert result_plain.column("label").to_pylist() == 
result_dict.column("label").to_pylist()
+
+
[email protected](
+    "table_properties,expected",
+    [
+        ({}, None),
+        (
+            {TableProperties.PARQUET_CDC_ENABLED: "true"},
+            {
+                "min_chunk_size": 
TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE_DEFAULT,
+                "max_chunk_size": 
TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE_DEFAULT,
+                "norm_level": TableProperties.PARQUET_CDC_NORM_LEVEL_DEFAULT,
+            },
+        ),
+        (
+            {
+                TableProperties.PARQUET_CDC_ENABLED: "true",
+                TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE: "4096",
+                TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE: "8192",
+                TableProperties.PARQUET_CDC_NORM_LEVEL: "2",
+            },
+            {"min_chunk_size": 4096, "max_chunk_size": 8192, "norm_level": 2},
+        ),
+    ],
+)
+def test_get_parquet_writer_kwargs_cdc(table_properties: dict[str, str], 
expected: dict[str, int] | None) -> None:
+    kwargs = _get_parquet_writer_kwargs(table_properties)
+    assert kwargs.get("use_content_defined_chunking") == expected
+
+
+def 
test_get_parquet_writer_kwargs_cdc_enabled_unsupported_pyarrow_version(monkeypatch:
 pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(pyarrow, "__version__", "17.0.0")
+    with pytest.raises(ImportError, match="pyarrow version >= 21.0.0"):
+        _get_parquet_writer_kwargs({TableProperties.PARQUET_CDC_ENABLED: 
"true"})
+
+
+def 
test_get_parquet_writer_kwargs_cdc_invalid_chunk_sizes_raises_from_pyarrow() -> 
None:
+    """PyArrow validates min/max chunk sizes itself; pyiceberg doesn't 
duplicate that check."""
+    kwargs = _get_parquet_writer_kwargs(
+        {
+            TableProperties.PARQUET_CDC_ENABLED: "true",
+            TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE: "8192",
+            TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE: "4096",
+        }
+    )
+    table = pa.table({"id": pa.array([1, 2, 3], type=pa.int32())})
+    with pytest.raises(OSError, match="max_chunk_size must be greater than 
min_chunk_size"):
+        with pq.ParquetWriter(pa.BufferOutputStream(), table.schema, **kwargs) 
as writer:
+            writer.write_table(table)

Review Comment:
   This assertion is brittle because it depends on PyArrow’s exact exception 
type and error message text, which may vary across PyArrow versions and 
platforms. Consider loosening the assertion by matching a more stable 
substring/regex (or checking for the presence of key terms) and/or catching a 
broader PyArrow exception class that reflects validation errors, to reduce test 
flakiness.



##########
tests/io/test_pyarrow.py:
##########
@@ -5462,3 +5463,89 @@ def 
test_dictionary_columns_produces_dict_encoded_output(tmpdir: str) -> None:
 
     # Values must be identical
     assert result_plain.column("label").to_pylist() == 
result_dict.column("label").to_pylist()
+
+
[email protected](
+    "table_properties,expected",
+    [
+        ({}, None),
+        (
+            {TableProperties.PARQUET_CDC_ENABLED: "true"},
+            {
+                "min_chunk_size": 
TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE_DEFAULT,
+                "max_chunk_size": 
TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE_DEFAULT,
+                "norm_level": TableProperties.PARQUET_CDC_NORM_LEVEL_DEFAULT,
+            },
+        ),
+        (
+            {
+                TableProperties.PARQUET_CDC_ENABLED: "true",
+                TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE: "4096",
+                TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE: "8192",
+                TableProperties.PARQUET_CDC_NORM_LEVEL: "2",
+            },
+            {"min_chunk_size": 4096, "max_chunk_size": 8192, "norm_level": 2},
+        ),
+    ],
+)
+def test_get_parquet_writer_kwargs_cdc(table_properties: dict[str, str], 
expected: dict[str, int] | None) -> None:
+    kwargs = _get_parquet_writer_kwargs(table_properties)
+    assert kwargs.get("use_content_defined_chunking") == expected
+
+
+def 
test_get_parquet_writer_kwargs_cdc_enabled_unsupported_pyarrow_version(monkeypatch:
 pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(pyarrow, "__version__", "17.0.0")
+    with pytest.raises(ImportError, match="pyarrow version >= 21.0.0"):
+        _get_parquet_writer_kwargs({TableProperties.PARQUET_CDC_ENABLED: 
"true"})
+
+
+def 
test_get_parquet_writer_kwargs_cdc_invalid_chunk_sizes_raises_from_pyarrow() -> 
None:
+    """PyArrow validates min/max chunk sizes itself; pyiceberg doesn't 
duplicate that check."""
+    kwargs = _get_parquet_writer_kwargs(
+        {
+            TableProperties.PARQUET_CDC_ENABLED: "true",
+            TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE: "8192",
+            TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE: "4096",
+        }
+    )
+    table = pa.table({"id": pa.array([1, 2, 3], type=pa.int32())})
+    with pytest.raises(OSError, match="max_chunk_size must be greater than 
min_chunk_size"):
+        with pq.ParquetWriter(pa.BufferOutputStream(), table.schema, **kwargs) 
as writer:
+            writer.write_table(table)
+
+
+def test_write_file_with_content_defined_chunking_enabled(tmp_path: Path) -> 
None:
+    """Writing a table with CDC enabled should forward 
use_content_defined_chunking to pq.ParquetWriter."""
+    from pyiceberg.table import WriteTask
+
+    table_schema = Schema(NestedField(1, "id", IntegerType(), required=False))
+    arrow_data = pa.table({"id": pa.array(range(1000), type=pa.int32())})
+
+    table_metadata = TableMetadataV2(
+        location=f"file://{tmp_path}",

Review Comment:
   The test constructs/parses `file://...` URIs via string formatting and 
`replace()`, which is not a valid file URI on Windows (e.g., drive letters) and 
can also mishandle paths with `file://` in other contexts. Use a proper URI 
generator (e.g., `tmp_path.as_uri()` for `location`) and parse 
`data_files[0].file_path` as a URI (e.g., via `urllib.parse.urlparse` or the 
project’s existing location parsing utilities) instead of `replace()`.



##########
tests/io/test_pyarrow.py:
##########
@@ -5462,3 +5463,89 @@ def 
test_dictionary_columns_produces_dict_encoded_output(tmpdir: str) -> None:
 
     # Values must be identical
     assert result_plain.column("label").to_pylist() == 
result_dict.column("label").to_pylist()
+
+
[email protected](
+    "table_properties,expected",
+    [
+        ({}, None),
+        (
+            {TableProperties.PARQUET_CDC_ENABLED: "true"},
+            {
+                "min_chunk_size": 
TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE_DEFAULT,
+                "max_chunk_size": 
TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE_DEFAULT,
+                "norm_level": TableProperties.PARQUET_CDC_NORM_LEVEL_DEFAULT,
+            },
+        ),
+        (
+            {
+                TableProperties.PARQUET_CDC_ENABLED: "true",
+                TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE: "4096",
+                TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE: "8192",
+                TableProperties.PARQUET_CDC_NORM_LEVEL: "2",
+            },
+            {"min_chunk_size": 4096, "max_chunk_size": 8192, "norm_level": 2},
+        ),
+    ],
+)
+def test_get_parquet_writer_kwargs_cdc(table_properties: dict[str, str], 
expected: dict[str, int] | None) -> None:
+    kwargs = _get_parquet_writer_kwargs(table_properties)
+    assert kwargs.get("use_content_defined_chunking") == expected
+
+
+def 
test_get_parquet_writer_kwargs_cdc_enabled_unsupported_pyarrow_version(monkeypatch:
 pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(pyarrow, "__version__", "17.0.0")
+    with pytest.raises(ImportError, match="pyarrow version >= 21.0.0"):
+        _get_parquet_writer_kwargs({TableProperties.PARQUET_CDC_ENABLED: 
"true"})
+
+
+def 
test_get_parquet_writer_kwargs_cdc_invalid_chunk_sizes_raises_from_pyarrow() -> 
None:
+    """PyArrow validates min/max chunk sizes itself; pyiceberg doesn't 
duplicate that check."""
+    kwargs = _get_parquet_writer_kwargs(
+        {
+            TableProperties.PARQUET_CDC_ENABLED: "true",
+            TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE: "8192",
+            TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE: "4096",
+        }
+    )
+    table = pa.table({"id": pa.array([1, 2, 3], type=pa.int32())})
+    with pytest.raises(OSError, match="max_chunk_size must be greater than 
min_chunk_size"):
+        with pq.ParquetWriter(pa.BufferOutputStream(), table.schema, **kwargs) 
as writer:
+            writer.write_table(table)
+
+
+def test_write_file_with_content_defined_chunking_enabled(tmp_path: Path) -> 
None:
+    """Writing a table with CDC enabled should forward 
use_content_defined_chunking to pq.ParquetWriter."""
+    from pyiceberg.table import WriteTask
+
+    table_schema = Schema(NestedField(1, "id", IntegerType(), required=False))
+    arrow_data = pa.table({"id": pa.array(range(1000), type=pa.int32())})
+
+    table_metadata = TableMetadataV2(
+        location=f"file://{tmp_path}",
+        last_column_id=1,
+        format_version=2,
+        schemas=[table_schema],
+        partition_specs=[PartitionSpec()],
+        properties={TableProperties.PARQUET_CDC_ENABLED: "true"},
+    )
+
+    task = WriteTask(
+        write_uuid=uuid.uuid4(),
+        task_id=0,
+        record_batches=arrow_data.to_batches(),
+        schema=table_schema,
+    )
+
+    with patch("pyiceberg.io.pyarrow.pq.ParquetWriter", 
wraps=pq.ParquetWriter) as mock_writer:
+        data_files = list(write_file(io=PyArrowFileIO(), 
table_metadata=table_metadata, tasks=iter([task])))
+
+    assert mock_writer.call_args.kwargs["use_content_defined_chunking"] == {
+        "min_chunk_size": TableProperties.PARQUET_CDC_MIN_CHUNK_SIZE_DEFAULT,
+        "max_chunk_size": TableProperties.PARQUET_CDC_MAX_CHUNK_SIZE_DEFAULT,
+        "norm_level": TableProperties.PARQUET_CDC_NORM_LEVEL_DEFAULT,
+    }
+
+    assert len(data_files) == 1
+    written_table = pq.read_table(data_files[0].file_path.replace("file://", 
""))

Review Comment:
   The test constructs/parses `file://...` URIs via string formatting and 
`replace()`, which is not a valid file URI on Windows (e.g., drive letters) and 
can also mishandle paths with `file://` in other contexts. Use a proper URI 
generator (e.g., `tmp_path.as_uri()` for `location`) and parse 
`data_files[0].file_path` as a URI (e.g., via `urllib.parse.urlparse` or the 
project’s existing location parsing utilities) instead of `replace()`.



##########
mkdocs/docs/configuration.md:
##########
@@ -85,6 +85,10 @@ Iceberg tables support table properties to configure table 
behavior.
 | `write.parquet.page-size-bytes`          | Size in bytes                     
 | 1MB                        | Set a target threshold for the approximate 
encoded size of data pages within a column chunk                                
                          |
 | `write.parquet.page-row-limit`           | Number of rows                    
 | 20000                      | Set a target threshold for the maximum number 
of rows within a column chunk                                                   
                       |
 | `write.parquet.dict-size-bytes`          | Size in bytes                     
 | 2MB                        | Set the dictionary page size limit per row 
group                                                                           
                          |
+| `write.parquet.content-defined-chunking.enabled` | Boolean                   
 | False                      | Enables content-defined chunking (CDC) for the 
Parquet writer, which produces stable page boundaries across appends. Requires 
`pyarrow>=21.0.0`, and raises at write time on older versions.     |
+| `write.parquet.content-defined-chunking.min-chunk-size` | Size in bytes      
 | 256KB                      | The minimum chunk size used for content-defined 
chunking                                                                        
                     |
+| `write.parquet.content-defined-chunking.max-chunk-size` | Size in bytes      
 | 1MB                        | The maximum chunk size used for content-defined 
chunking                                                                        
                     |

Review Comment:
   The documented defaults use `256KB` / `1MB`, but the actual defaults in code 
are `256 * 1024` (256 KiB) and `1024 * 1024` (1 MiB). To avoid ambiguity, 
update the docs to reflect binary units (e.g., `256KiB` / `1MiB`) or include 
the exact byte counts.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to