qzyu999 opened a new issue, #2718:
URL: https://github.com/apache/iceberg-rust/issues/2718
### Is your feature request related to a problem or challenge?
PyIceberg (iceberg-python) needs to perform compute-heavy table operations —
compaction, equality delete resolution, copy-on-write file rewrites, orphan
file deletion — but has no path to execute these with bounded memory through
the existing `pyiceberg-core` bindings.
**Current state of `pyiceberg-core`:**
- `pyiceberg_core.datafusion` — exposes `IcebergStaticTableProvider` as a
read-only DataFusion `TableProvider` via PyCapsule FFI
- `pyiceberg_core.transform` — partition transform functions
- `pyiceberg_core.manifest` — manifest reading utilities
**What's missing:**
There is no way to trigger a full bounded-memory execution pipeline (scan →
transform → write) from Python. The existing `TableProvider` FFI only supports
reads. For write/compute operations, data would need to cross the Python↔Rust
FFI boundary per-batch, which defeats bounded-memory guarantees (Python's
address space holds the data, preventing Rust-side memory management).
**The consequence for PyIceberg:**
- `table.scan().to_arrow()` on tables with equality deletes raises
`ValueError` (completely unreadable)
- `table.compact()` is not implemented (no bounded-memory sort)
- `Transaction.delete()` OOMs on large Parquet files (loads entire file into
memory)
- Orphan file deletion OOMs on tables with millions of files
### Describe the solution you'd like
Add a new `execution` submodule to the Python bindings
(`bindings/python/src/execution.rs`) that exposes operation-level functions via
PyO3. Each function:
1. Accepts operation parameters from Python (file paths, filter expressions,
memory limit)
2. Creates a bounded-memory DataFusion session internally (using
`FairSpillPool`)
3. Constructs and executes a DataFusion plan entirely in Rust (GIL released)
4. Returns only metadata (new file paths, record counts) to Python — not
bulk Arrow data
### Design principle: Operation-level FFI
The boundary crosses at the **operation** level, not the record level:
```python
# What we want (operation-level — all compute in Rust):
result = pyiceberg_core.execution.execute_compaction(
metadata_location="s3://bucket/metadata/v3.metadata.json",
file_io_properties={...},
files_to_compact=[...], # serialized DataFile JSON
memory_limit="512MB",
)
# Returns: CompactionResult(new_files=[...], total_record_count=N)
# Python only gets back metadata for commit. Data never leaves Rust.
```
This ensures DataFusion's memory pool manages all data in Rust's address
space, and Python threads are not blocked during execution (GIL released by
Tokio).
### Proposed functions
```python
# pyiceberg_core.execution (type stubs)
def execute_cow_rewrite(
metadata_location: str,
file_io_properties: dict[str, str],
files_to_rewrite: list[str], # DataFile JSON
filter_expression: str, # Iceberg expression
keep_matching: bool,
memory_limit: str | None = None, # e.g., "512MB"
) -> CowRewriteResult: ...
def execute_compaction(
metadata_location: str,
file_io_properties: dict[str, str],
files_to_compact: list[str], # DataFile JSON
target_file_size_bytes: int | None = None,
sort_columns: list[str] | None = None,
memory_limit: str | None = None,
) -> CompactionResult: ...
def execute_equality_resolution(
data_file_paths: list[str],
eq_delete_file_paths: list[str],
equality_field_names: list[str],
file_io_properties: dict[str, str],
memory_limit: str | None = None,
) -> list[RecordBatch]: ...
def execute_antijoin_paths(
storage_paths: list[str],
valid_paths: list[str],
memory_limit: str | None = None,
) -> list[str]: ...
```
### Result types
```python
class CowRewriteResult:
new_files: list[str] # DataFile JSON for commit
total_record_count: int
total_file_size_bytes: int
class CompactionResult:
new_files: list[str] # DataFile JSON for commit
total_record_count: int
total_file_size_bytes: int
input_files_count: int
```
### How PyIceberg uses this
Python handles orchestration (file selection, commit). Rust handles compute
(sort, join, write):
```python
# In PyIceberg's table.compact():
from pyiceberg_core.execution import execute_compaction
# 1. Python selects files to compact (manifest-based planning)
files = self._select_files_for_compaction(filter)
# 2. Rust executes the sort + rewrite with bounded memory
result = execute_compaction(
metadata_location=self.metadata_location,
file_io_properties=self.io.properties,
files_to_compact=[serialize(f) for f in files],
sort_columns=self.sort_order(),
memory_limit="512MB",
)
# 3. Python commits the replacement atomically
with self.transaction() as tx:
tx.overwrite(old_files=files, new_files=result.new_files)
```
---
## Implementation approach
### Module structure
```rust
// bindings/python/src/execution.rs
#[pyclass] struct PyCowRewriteResult { ... }
#[pyclass] struct PyCompactionResult { ... }
#[pyfunction] fn execute_cow_rewrite(...) -> PyResult<PyCowRewriteResult> {
... }
#[pyfunction] fn execute_compaction(...) -> PyResult<PyCompactionResult> {
... }
#[pyfunction] fn execute_equality_resolution(...) -> PyResult<PyObject> {
... }
#[pyfunction] fn execute_antijoin_paths(...) -> PyResult<Vec<String>> { ... }
pub fn register_module(py: Python<'_>, m: &Bound<'_, PyModule>) ->
PyResult<()> { ... }
```
### Registration in `lib.rs`
```rust
mod execution;
#[pymodule]
fn pyiceberg_core_rust(py: Python<'_>, m: &Bound<'_, PyModule>) ->
PyResult<()> {
datafusion_table_provider::register_module(py, m)?;
transform::register_module(py, m)?;
manifest::register_module(py, m)?;
execution::register_module(py, m)?; // NEW
Ok(())
}
```
### Internal execution pattern (for each function)
```rust
fn execute_compaction(...) -> PyResult<PyCompactionResult> {
let rt = runtime(); // shared Tokio runtime (existing pattern)
rt.block_on(async {
// 1. Create bounded session (FairSpillPool + DiskManager)
let ctx =
create_bounded_session(BoundedSessionConfig::new(memory_bytes))?;
// 2. Load table via StaticTable (no catalog needed — just metadata
file)
let file_io = FileIOBuilder::new(factory).with_props(props).build();
let table = StaticTable::from_metadata_file(&path, ident,
file_io).await?;
// 3. Build DataFusion plan:
// IcebergTableScan(specific files) → SortExec → IcebergWriteExec
// (SortExec spills to disk automatically when FairSpillPool is
exhausted)
// 4. Execute plan (all data stays in Rust address space)
let results = collect(plan, ctx.task_ctx()).await?;
// 5. Extract DataFile metadata from IcebergWriteExec output
// Return to Python (only metadata crosses FFI, not bulk data)
Ok(PyCompactionResult { new_files, total_record_count, ... })
})
}
```
---
## Phased delivery
This can be delivered incrementally:
| Phase | Scope | Blocked on |
|:------|:------|:-----------|
| **Phase 1** | Module structure, `register_module`, result types, function
signatures with `todo!()` bodies | Nothing |
| **Phase 2** | `execute_antijoin_paths` implementation (simplest — just
register arrays, anti-join, collect) | Bounded session helper |
| **Phase 3** | `execute_equality_resolution` implementation (register
Parquet files, anti-join, return Arrow batches) | Bounded session helper |
| **Phase 4** | `execute_cow_rewrite` implementation (scan → filter → write
via IcebergWriteExec) | Bounded session + OverwriteAction/RewriteFiles for
commit |
| **Phase 5** | `execute_compaction` implementation (scan → sort → write via
IcebergWriteExec) | Bounded session + OverwriteAction/RewriteFiles for commit |
Phase 1 can ship immediately as a standalone PR to get early API feedback.
---
## Related issues
- [#2269](https://github.com/apache/iceberg-rust/issues/2269) — [EPIC]
Implement Missing Write Actions (motivation: enable end-to-end native writes
from Python)
- [#1607](https://github.com/apache/iceberg-rust/issues/1607) — Add
RewriteFiles support (prerequisite for compaction/CoW commit in Phases 4-5)
- [#2186](https://github.com/apache/iceberg-rust/issues/2186) — CoW and MoR
support (broader epic)
- [#1797](https://github.com/apache/iceberg-rust/issues/1797) — Reduce the
need for iceberg-rust forks (this feature makes iceberg-rust directly usable
from Python for production workloads)
### PyIceberg issues this unblocks
-
[iceberg-python#1210](https://github.com/apache/iceberg-python/issues/1210) —
Equality delete read support
-
[iceberg-python#1092](https://github.com/apache/iceberg-python/issues/1092) —
Data compaction
-
[iceberg-python#3270](https://github.com/apache/iceberg-python/issues/3270) —
Equality delete data correctness
-
[iceberg-python#1200](https://github.com/apache/iceberg-python/issues/1200) —
Orphan file deletion (OOM risk)
### Willingness to contribute
I would be willing to contribute to this feature with guidance from the
Iceberg Rust community
--
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]