Fix DataFile spec ID and reject unknown record fields - #3954
Conversation
Reject unknown record fields while retaining spec_id as transient DataFile context. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use the internal table format selector now that unknown DataFile fields are rejected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Let the v2 manifest writer project the canonical record instead of constructing a version-specific positional layout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep transient DataFile context out of the expected Avro payload. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the canonical DataFile name distinct from the explicit v1 record and order version selection first. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| @classmethod | ||
| def from_args(cls, _table_format_version: TableVersion = DEFAULT_READ_VERSION, **arguments: Any) -> DataFile: | ||
| def from_args( | ||
| cls, _table_format_version: TableVersion = DEFAULT_READ_VERSION, *, spec_id: int | None = None, **arguments: Any |
There was a problem hiding this comment.
explicitly add spec_id to the function.
its called in
iceberg-python/pyiceberg/io/pyarrow.py
Line 2771 in 308768d
iceberg-python/pyiceberg/io/pyarrow.py
Line 2909 in 308768d
I think this is better. Otherwise caller has to set it after constructing DataFile. For example
data_file = DataFile.from_args()
data_file.spec_id = spec_id
| if spec_id is not None: | ||
| data_file.spec_id = spec_id | ||
| return data_file |
There was a problem hiding this comment.
this is using DataFile's own setter
iceberg-python/pyiceberg/manifest.py
Lines 551 to 560 in 562d3af
|
|
||
| @classmethod | ||
| def _bind(cls, struct: StructType, **arguments: Any) -> Self: | ||
| field_names = {field.name for field in struct.fields} |
There was a problem hiding this comment.
including this fix as part of the PR.
This is the footgun that was silently dropping fields. We now check for unknown fields.
This caught format_version=2, in tests/integration/test_rest_manifest.py which is invalid
| "spec_id": 3, | ||
| } | ||
| data_file_v2 = DataFile.from_args(**data_file_dict) # type: ignore | ||
| data_file = DataFile.from_args(content=DataFileContent.DATA, **common_data_file_args) # type: ignore |
There was a problem hiding this comment.
content is a V2 field.
common_data_file_args is shared between v1 and v2 in this test file
| data_file_v1 = DataFile.from_args(**data_file_dict, _table_format_version=format_version) | ||
| data_file_v1 = DataFile.from_args( | ||
| _table_format_version=format_version, | ||
| block_size_in_bytes=DEFAULT_BLOCK_SIZE, |
There was a problem hiding this comment.
block_size_in_bytes is required in V1.
Resolve the v2 manifest fixture conflict after referenced_data_file was added to the v2 schema. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| wrapped_entry_v2.data_file = wrapped_data_file_v2_debug | ||
| wrapped_entry_v2_dict = todict(wrapped_entry_v2, [field.name for field in test_spec.fields]) | ||
| for field in ("first_row_id", "content_offset", "content_size_in_bytes"): | ||
| for field in ("first_row_id", "content_offset", "content_size_in_bytes", "spec_id"): |
There was a problem hiding this comment.
spec_id is not serialized so we need to remove it for this comparison to work
| test_schema = table_test_all_types.schema() | ||
| test_spec = table_test_all_types.spec() | ||
| wrapped_data_file_v2_debug = DataFile.from_args( | ||
| format_version=2, |
There was a problem hiding this comment.
this is not a valid field, the typedef.py change caught it 😄
| "upper_bounds": {1: b"zzzzzzzzzzzzzzzz"}, | ||
| "key_metadata": b"\xde\xad\xbe\xef", | ||
| "split_offsets": [4, 133697593], | ||
| "equality_ids": [], |
There was a problem hiding this comment.
equality_ids is not a valid V1 field.
common_data_file_args is shared between v1 and v2 in this test file
geruh
left a comment
There was a problem hiding this comment.
lgtm, and aligns with java impl. The spec id is omitted wrt the manifests serialized record.
Summary
This PR fixes two related constructor issues:
DataFile.from_argsnow setsspec_idwhen it is provided.spec_idremains transient runtime context and is not included in the serializeddata_filerecord.Record._bindnow raisesTypeErrorfor fields that are not part of the selected struct instead of silently dropping them.Motivation
DataFile.from_args(spec_id=...)previously accepted the argument, but_binddiscarded it becausespec_idbelongs to the containing manifest rather than the serialized data-file schema. The resultingDataFilehad no spec context, and accessingspec_idcould raiseAttributeError.More generally, silently ignoring unknown fields is a footgun: misspelled, unsupported, or wrong-version arguments appear to work while producing an incomplete record. Validating arguments in
_bindmakes these mistakes fail immediately for all record types.Tests
make lintmake test PYTEST_ARGS="-q"pytest -v -x -m integration tests/integration/test_rest_manifest.py::test_write_sample_manifest