Repository navigation
fix(compaction): rewrite complete source files - #2288
Conversation
Signed-off-by: Matt Faltyn <faltyn.matthew@gmail.com>
Signed-off-by: Matt Faltyn <faltyn.matthew@gmail.com>
laskoviymishka
left a comment
There was a problem hiding this comment.
Thanks @mattfaltyn for the split x partial regression. As I read the base path, both split cases fail there: partial progress commits 1 of 12 rows, and atomic fails at commit because the same file is queued for deletion 12 times. I think this is fine overall, but I'd like to strengthen the first-task test a bit before approving. The residual question on the collapse loop is worth covering too, though it isn't a blocker.
| } | ||
| path := task.File.FilePath() | ||
| if _, ok := seenFiles[path]; ok { | ||
| continue |
There was a problem hiding this comment.
Should PlanCompaction return ErrInvalidOperation when a later task for the same path carries different delete files or FirstRowID? Today it keeps the first and skips the rest without comparing, while ExecuteCompactionGroup rejects a duplicate path outright. Splits from one scan are copies of a single task, so nothing in-repo hits this. The check would enforce the "one coherent scan plan" precondition instead of only documenting it.
| } | ||
| seenFiles[path] = struct{}{} | ||
| task.Start = 0 | ||
| task.Length = task.File.FileSizeBytes() |
There was a problem hiding this comment.
Could this also set task.Residual = nil, to match Java's planner, which scans with ignoreResiduals()? As it stands, a task planned from Scan(WithRowFilter(...)) with a data-column predicate keeps its residual through planning and is only rejected later by the new check in ExecuteCompactionGroup. Analyze scans unfiltered, so this isn't a blocker. It's more a question of where we want that contract to live.
| first.FirstRowID, first.DataSequenceNumber = &firstRowID, &sequence | ||
| first.Residual = iceberg.AlwaysTrue{} | ||
| first.Length = file.size / 2 | ||
| second := first |
There was a problem hiding this comment.
Could the two same-path tasks differ in more than the range? second := first copies the DV, FirstRowID and sequence number, and require.Contains compares with reflect.DeepEqual, so this passes whether the planner keeps the earlier task or the later one. Since second sits first in tasks, giving first its own FirstRowID after the copy would pin the first-task rule the doc comment describes.
Signed-off-by: Matt Faltyn <faltyn.matthew@gmail.com>
Signed-off-by: Matt Faltyn <faltyn.matthew@gmail.com>
|
Thanks @laskoviymishka! All three points are addressed: the planner rejects conflicting delete or lineage metadata, clears residuals for whole-file rewrites, and the first-task test now checks object identity. Added regressions for metadata conflicts and filtered rewrites in both atomic and partial-progress modes. You're right that both split cases fail on the base. Full local checks and CI pass. |
zeroshade
left a comment
There was a problem hiding this comment.
Thanks, this closes both loss paths: split ranges were packed as independent files, and residual filters dropped rows while the whole source file was replaced. I merged it with current main locally and the tests pass. With compaction.go and rewrite_data_files.go reverted, TestCompactionPreservesSplitFileRows fails in all four split cases and in both unsplit filtered cases, so it pins both.
One non-blocking nit: sameCompactionMetadata compares delete files with reflect.DeepEqual, but dataFile fills some fields lazily (initPartition/initColumnStats and the stats maps). Two identical delete files stop comparing equal once one has had its bounds or partition read. Local split tasks share pointers, so DeepEqual short-circuits, but delete files decoded from separate REST responses don't, so it could raise a spurious "different metadata" error. Comparing identity fields (path, content offset/size) would be sturdier, in a follow-up if you like. LGTM.
…61023 Resolve the collectGeoColumns doc conflict with apache#2303, rename the AddFiles call added by apache#2288's test, and restore the compatibility note from 3b5bc47 dropped by the release-notes sync.
Fixes #2287.
Problem
Parquet scan planning can return multiple byte-range tasks for one source file. Compaction counted and packed those ranges as independent files, then read only each range while scheduling the entire source file for replacement. Partial-progress commits could therefore publish one range's rows and fail on a later group, leaving unread rows absent from the active snapshot.
Change
Tasks passed to the planner come from one coherent scan plan. The fix preserves the exported API; compaction reads complete source files, with concurrency across files.
Validation
The new row-preservation regression fails on the base: the split partial-progress case returns an error after committing only 1 of 12 rows; the unsplit controls pass. The regression preserves all twelve rows with this change, with both atomic and partial-progress execution.
Passed locally (Go checks use Go 1.26.3):
make test,make test-race,make test-assert, andmake vet(including integration-tagged code).make lint(golangci-lint v2.14.0; zero issues).go build ./...andGOOS=linux GOARCH=s390x CGO_ENABLED=0 go build ./....dev/check-api-compat.sh origin/main(no incompatible API changes).dev/check-license,mdbook build, andgit diff --check.make integration-testagainst Spark 3.5 and Spark 4.0 in a local Linux/arm64 Docker runner. The Spark 4 run disables test-result caching.make integration-external-restagainst Apache Polaris 1.8.0.CI results are available in the PR checks.