Repository navigation
Conversation
AP24110011695
left a comment
There was a problem hiding this comment.
Verified locally (Windows, Python 3.12):
test_partial_delete_keeps_earlier_deletes_outside_the_filterpasses on this branch across all three catalog fixtures, and fails on the pre-change code with the exact resurrection the issue describes ([2, 3]vs[3]) — reproduced by swapping only thepyiceberg/source tree.- Regression slice on this branch: full
tests/catalog/test_catalog_behaviors.py+tests/table/test_delete_file_index.py, 721 passed. - Checked the two behavioral risks I was worried about: (1) the direct
ManifestGroupPlannerconstruction drops scanoptions, but the only consumed option isinclude_empty_files, andTransaction._scannever set options anyway — so both paths seeEMPTY_DICT. (2) SkippingDataScan.plan_files()also skips the server-side planning branch, butTransaction._scanpasses no catalog, so_should_use_server_side_planning()was always false here — the claim in the description holds. - The
keep_delete_fileswrapper threads into_open_manifestcorrectly and sequence-number manifest filtering is unchanged; data entries are still metrics-pruned, so rewrite targeting is unaffected.
Good catch on the asymmetry between filtered reads (where bounds-pruning delete files is safe) and full-file rewrites (where it silently resurrects rows) — and for fixing someone else's issue rather than leaving it. Nice work.
| evaluate_metrics = self._build_metrics_evaluator() | ||
| metrics_evaluator = evaluate_metrics | ||
| if not self.prune_delete_files: | ||
|
|
||
| def keep_delete_files(data_file: DataFile) -> bool: | ||
| return data_file.content != DataFileContent.DATA or evaluate_metrics(data_file) | ||
|
|
||
| metrics_evaluator = keep_delete_files |
There was a problem hiding this comment.
Since a manifest holds either data or delete files, could we pick the evaluator per manifest instead of checking content on every entry? That also avoids having both evaluate_metrics and metrics_evaluator:
metrics_evaluator = self._build_metrics_evaluator()
...
(
self.io,
manifest,
partition_evaluators[manifest.partition_spec_id],
metrics_evaluator if self.prune_delete_files or manifest.content == ManifestContent.DATA else lambda _: True,
)There was a problem hiding this comment.
Done, thanks! Switched to picking the evaluator per manifest, and rebased on main to clear the conflicts.
A partial delete reads each candidate file in full and writes back the surviving rows, but planned the rewrite with the delete filter, which also prunes delete files by column bounds. Rows removed by a pruned delete file came back in the rewritten file. Plan the rewrite without pruning delete files; scans are unchanged.
02087f2 to
7b109f3
Compare
| row_filter: str | BooleanExpression = ALWAYS_TRUE, | ||
| case_sensitive: bool = True, | ||
| options: Properties = EMPTY_DICT, | ||
| prune_delete_files: bool = True, |
There was a problem hiding this comment.
nit: can we use something like
_ignore_residuals=True,instead? It follows the repo's conventions more closely:
- The
_prefix keeps it out of the public API, like_isolation_operation;ManifestGroupPlannershipped in 0.12.0. - It matches Java's
ignoreResiduals: the caller says files are read in full, and the planner keeps every delete file and sets residuals to always true.
There was a problem hiding this comment.
Done, renamed to ignore_residuals (defaulting to False).
|
Follow-up verification on the two new commits (7b10913 + d334371):
The NaN-partition predicate addition ( |
Closes #4291.
Rationale for this change
A partial
Table.deleterewrites each data file that might hold matching rows. It reads the whole file withAlwaysTrue()and writes back the rows that don't match. The files to rewrite were planned with the delete filter, though, and that plan also leaves out delete files whose column bounds can't match the filter. For a filtered read that's correct, because the residual drops those rows anyway. For a full-file rewrite it isn't: the left-out delete files are never applied, so the rows they deleted come back in the new data file.overwrite(..., overwrite_filter=...),dynamic_partition_overwriteandupsertgo throughTransaction.delete, so they're affected too.The fix keeps the delete filter for choosing data files, but stops pruning delete files by their column bounds when planning a rewrite:
ManifestGroupPlannertakesprune_delete_files: bool = True. When it'sFalse, the metrics evaluator is applied to data entries only. Partition pruning is unchanged, since a delete file only applies to data files in its own partition.Transaction.deleteplans its rewrite withprune_delete_files=False. Its scan never has a catalog, so it always planned locally, and server-side planning isn't involved.Scans keep the default, so reads and their planning cost are unchanged.
Are these changes tested?
Yes.
test_partial_delete_keeps_earlier_deletes_outside_the_filterintests/catalog/test_catalog_behaviors.pyruns against each catalog fixture and follows case 1 from the issue:(2, 'b'), which gives it boundsk = 2;k = 1leaves that delete file out;delete("k = 1")and checks onlyk = 3is left.Without the fix the final assertion fails with
[2, 3] == [3]: the deleted row is back. PyIceberg can't write delete manifests yet, so the test's helper subclassesManifestWriterV2, as the reproduction in the issue does. Once #4002 lands, the helper can passcontent=ManifestContent.DELETESinstead.The equality-delete case from the issue isn't tested here, because PyIceberg can't read equality deletes. With this change, a partial delete over a file that has an equality delete now reaches the existing "does not yet support equality deletes" error instead of silently writing the deleted rows back.
Are there any user-facing changes?
Partial deletes, filtered overwrites and upserts no longer resurrect rows removed by earlier delete files. A partial delete on a table with equality delete files that only the column bounds used to exclude now raises the existing unsupported-equality-deletes error, rather than writing incorrect data.
Not in this PR: the issue also points out that the retry conflict check (
_filter_manifest_entriesintable/update/validate.py) prunes delete entries the same way. That's a separate code path, so I'll follow up on it separately.