Repository navigation
feat(encryption): [11/N] Read encrypted Parquet data files #4309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,7 @@ | |
| from typing_extensions import override | ||
|
|
||
| from pyiceberg.conversions import to_bytes | ||
| from pyiceberg.encryption.key_metadata import StandardKeyMetadata | ||
| from pyiceberg.exceptions import ResolveError | ||
| from pyiceberg.expressions import AlwaysTrue, BooleanExpression, BoundIsNaN, BoundIsNull, BoundTerm, Not, Or | ||
| from pyiceberg.expressions.literals import Literal | ||
|
|
@@ -1146,9 +1147,10 @@ def _expression_to_complementary_pyarrow(expr: BooleanExpression, schema: Schema | |
| return expression_to_pyarrow(preserve_expr, schema) | ||
|
|
||
|
|
||
| @lru_cache | ||
| def _get_file_format(file_format: FileFormat, **kwargs: dict[str, Any]) -> ds.FileFormat: | ||
| def _create_arrow_read_format(file_format: FileFormat, key_metadata: bytes | None, **kwargs: Any) -> ds.FileFormat: | ||
| if file_format == FileFormat.PARQUET: | ||
| if key_metadata is not None: | ||
| kwargs["decryption_properties"] = _parquet_decryption_properties(key_metadata) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
https://arrow.apache.org/docs/python/generated/pyarrow.parquet.ParquetFile.html |
||
| return ds.ParquetFileFormat(**kwargs) | ||
| elif file_format == FileFormat.ORC: | ||
| # ORC doesn't support pre_buffer and buffer_size parameters | ||
|
|
@@ -1158,11 +1160,31 @@ def _get_file_format(file_format: FileFormat, **kwargs: dict[str, Any]) -> ds.Fi | |
| raise ValueError(f"Unsupported file format: {file_format}") | ||
|
|
||
|
|
||
| def _check_encryption_supported(data_file: DataFile) -> None: | ||
| if data_file.key_metadata is not None and data_file.file_format != FileFormat.PARQUET: | ||
| raise ValueError(f"Cannot read encrypted {data_file.file_format} file {data_file.file_path}: only Parquet is supported") | ||
|
|
||
|
|
||
| def _parquet_decryption_properties(key_metadata: bytes) -> pq.FileDecryptionProperties: | ||
| try: | ||
| from pyarrow.parquet.encryption import create_decryption_properties | ||
| except ImportError as e: | ||
| raise ImportError(f"pyarrow >= 25.0.0 required to read encrypted Parquet files, found {pyarrow.__version__}") from e | ||
|
|
||
| metadata = StandardKeyMetadata.from_bytes(key_metadata) | ||
| return create_decryption_properties(metadata.encryption_key, aad_prefix=metadata.aad_prefix) | ||
|
|
||
|
|
||
| def _read_deletes(io: FileIO, data_file: DataFile) -> dict[str, pa.ChunkedArray]: | ||
| _check_encryption_supported(data_file) | ||
| if data_file.file_format == FileFormat.PARQUET: | ||
| with io.new_input(data_file.file_path).open() as fi: | ||
| delete_fragment = _get_file_format( | ||
| data_file.file_format, dictionary_columns=("file_path",), pre_buffer=True, buffer_size=ONE_MEGABYTE | ||
| delete_fragment = _create_arrow_read_format( | ||
| data_file.file_format, | ||
| data_file.key_metadata, | ||
| dictionary_columns=("file_path",), | ||
| pre_buffer=True, | ||
| buffer_size=ONE_MEGABYTE, | ||
| ).make_fragment(fi) | ||
| table = ds.Scanner.from_fragment(fragment=delete_fragment).to_table() | ||
| table = table.unify_dictionaries() | ||
|
|
@@ -1172,7 +1194,7 @@ def _read_deletes(io: FileIO, data_file: DataFile) -> dict[str, pa.ChunkedArray] | |
| } | ||
| elif data_file.file_format == FileFormat.ORC: | ||
| with io.new_input(data_file.file_path).open() as fi: | ||
| delete_fragment = _get_file_format(data_file.file_format).make_fragment(fi) | ||
| delete_fragment = _create_arrow_read_format(data_file.file_format, data_file.key_metadata).make_fragment(fi) | ||
| table = ds.Scanner.from_fragment(fragment=delete_fragment).to_table() | ||
| # For ORC, file_path columns are not dictionary-encoded, so we use unique() directly | ||
| return { | ||
|
|
@@ -1712,10 +1734,11 @@ def _task_to_record_batches( | |
| downcast_ns_timestamp_to_us: bool | None = None, | ||
| dictionary_columns: tuple[str, ...] = (), | ||
| ) -> Iterator[pa.RecordBatch]: | ||
| _check_encryption_supported(task.file) | ||
| format_kwargs: dict[str, Any] = {"pre_buffer": True, "buffer_size": ONE_MEGABYTE * 8} | ||
| if dictionary_columns and task.file.file_format == FileFormat.PARQUET: | ||
| format_kwargs["dictionary_columns"] = dictionary_columns | ||
| arrow_format = _get_file_format(task.file.file_format, **format_kwargs) | ||
| arrow_format = _create_arrow_read_format(task.file.file_format, task.file.key_metadata, **format_kwargs) | ||
| with io.new_input(task.file.file_path).open() as fin: | ||
| fragment = arrow_format.make_fragment(fin) | ||
| physical_schema = fragment.physical_schema | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for keeping this small 😄 as i mentioned in the other pr, would be good to add a script to be able to regenerate these test files
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me open an issue so I don’t forget |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could consider making
key_metadatarequired so future callers wont forget to include itThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is sorted now right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks great, thanks!