feat: add rewrite manifests - #774
Conversation
18bade1 to
7de6949
Compare
5c1ee1f to
dd9c7e6
Compare
| auto new_writer = [this, &schema](const RewriteWriter& rewrite_writer) | ||
| -> Result<std::unique_ptr<ManifestWriter>> { | ||
| std::optional<int64_t> first_row_id = std::nullopt; | ||
| if (base().format_version >= 3 && rewrite_writer.content == ManifestContent::kData) { |
There was a problem hiding this comment.
If the table was upgraded from v2, existing live files may still have null per-file row IDs. Then ManifestEntryAdapterV3::GetFirstRowId falls back to the writer-level value, so those files get 0, and the manifest list skips assignment because the manifest now has a non-null first_row_id. Java leaves the rewrite writer’s manifest firstRowId null via newManifestWriter, allowing manifest-list assignment when needed after upgrade.
There was a problem hiding this comment.
Good catch. I removed the non-null rewrite writer first_row_id logic and added a v2 -> v3 regression test.
| const bool has_direct_replacements = !deleted_manifests_.empty() || | ||
| !added_manifests_.empty() || | ||
| !rewritten_added_manifests_.empty(); | ||
| if (!cluster_by_func_ && !predicate_ && has_direct_replacements) { |
There was a problem hiding this comment.
Java does not rewrite at all when clusterByFunc == null, while this rewrites when predicate_ is set. Is it intentional?
There was a problem hiding this comment.
Not intentional. I changed RequiresRewrite to match Java.
dd9c7e6 to
3736bc1
Compare
| rewrite_writer.content); | ||
| }; | ||
|
|
||
| std::vector<ManifestFile> result; |
There was a problem hiding this comment.
Closed manifests are kept only in this local result vector until Rewrite() returns, and new_manifests_ is assigned only after the call succeeds in Apply(). If a later length(), WriteExistingEntry, Close(), or
ToManifestFile() returns an error, finalization/cleanup no longer has the already-created manifest paths.
Could we either append closed manifests to member state as soon as they are produced, or clean up the local partial result on the error path? It would also be good to add a test that injects an apply-time manifest writer failure after at least one rolled manifest has been closed.
There was a problem hiding this comment.
Good catch. I'll track rewritten manifests once each writer is closed so that apply-time failures can clean up already-created manifests. Will add a regression test too.
3736bc1 to
ecc0058
Compare
| /// snapshot and reattempting the commit. | ||
| class ICEBERG_EXPORT RewriteManifests : public SnapshotUpdate { | ||
| public: | ||
| using ClusterByFunc = std::function<std::string(const DataFile&)>; |
There was a problem hiding this comment.
Why this function returns string instead of size_t just like what C++ hash function does?
| std::vector<ManifestFile> deleted_manifests_; | ||
| std::unordered_set<std::string> deleted_manifest_paths_; |
There was a problem hiding this comment.
We should combine deleted_manifests_ and deleted_manifest_paths_ into a single unordered_set<ManifestFile> by using pure ManifestFile.path in its hash function.
There was a problem hiding this comment.
Same for rewritten_manifests_ and rewritten_manifest_paths_ below.
|
|
||
| struct RewriteWriter { | ||
| std::shared_ptr<PartitionSpec> spec; | ||
| ManifestContent content; |
There was a problem hiding this comment.
Why do we need this? This class only rewrites data manifests, right?
| return *this; | ||
| } | ||
|
|
||
| Status RewriteManifests::ValidateTargetBranch(const std::string& branch) const { |
There was a problem hiding this comment.
Why do we need this? I didn't find this in the Java code.
| ICEBERG_PRECHECK(snapshot != nullptr, | ||
| "Cannot rewrite manifests without a current snapshot"); | ||
|
|
||
| SnapshotCache cached_snapshot(snapshot.get()); |
There was a problem hiding this comment.
I have to admit that SnapshotCache is not a good name. Perhaps we should rename it to SnapshotReader or something.
| if (!ctx_->transaction) { | ||
| // Table-created path: no transaction exists yet, create a temporary one. | ||
| ICEBERG_ASSIGN_OR_RAISE(auto txn, Transaction::Make(ctx_)); | ||
| auto self = weak_from_this().lock(); |
There was a problem hiding this comment.
Why do we need this kind of change?
There was a problem hiding this comment.
Review comment from Codex: This registers standalone updates for commit retries, but explicit transaction updates still return from txn->Apply without calling Finalize on failure. A failed rewrite can leave generated manifests behind. Please finalize or abort transaction-owned updates on apply failure and add a Transaction::NewRewriteManifests fault-injection test.
| return self.AddError(ErrorKind::kInvalidArgument, "Branch name cannot be empty"); | ||
| } | ||
|
|
||
| if (auto status = static_cast<SnapshotUpdate&>(self).ValidateTargetBranch(branch); |
| return {}; | ||
| } | ||
|
|
||
| Result<ManifestFile> RewriteManifests::CopyManifest(const ManifestFile& manifest) { |
There was a problem hiding this comment.
Let's move this function to src/iceberg/manifest/manifest_util_internal.h and follow the style of CopyAppendManifest?
|
|
||
| ICEBERG_ASSIGN_OR_RAISE( | ||
| auto writer, | ||
| ManifestWriter::MakeWriter(base().format_version, SnapshotId(), ManifestPath(), |
There was a problem hiding this comment.
The generated path is lost after MakeWriter succeeds. If writing, closing, or ToManifestFile fails, the output is not reliably closed or tracked for cleanup. Please retain the path and manage it with RAII until the manifest is completed successfully.
Unfortunately, CopyAppendManifest in the manifest_util_internal.h has the same issue.
| "Manifest entry in {} is missing data_file", | ||
| manifest_entry.manifest.manifest_path); | ||
| auto key = WriterKey{ | ||
| cluster_by_func_ ? cluster_by_func_(*entry.data_file) : std::string{}, |
There was a problem hiding this comment.
cluster_by_func_ is user code and may throw. An exception here bypasses the close-all loop and leaves partial manifests untracked. Please make writer cleanup RAII and add a throwing-callback test.
wgtmac
left a comment
There was a problem hiding this comment.
These comments are about the standalone update lifecycle:
- Registration must be mandatory so commit retries reapply the update.
- The temporary transaction must stay detached so its expired weak pointer does not break a later
Commit(). - No-op commits must still call
Finalize()because cleanup is now delegated toTransaction.
Together, these keep one ownership, retry, and finalization path for standalone and explicit transactions.
| auto self = weak_from_this().lock(); | ||
| if (self) { | ||
| ICEBERG_RETURN_UNEXPECTED(txn->AddUpdate(self)); | ||
| } |
There was a problem hiding this comment.
Please require shared ownership and always register the update.
| auto self = weak_from_this().lock(); | |
| if (self) { | |
| ICEBERG_RETURN_UNEXPECTED(txn->AddUpdate(self)); | |
| } | |
| auto self = weak_from_this().lock(); | |
| ICEBERG_PRECHECK(self != nullptr, | |
| "PendingUpdate must be owned by std::shared_ptr"); | |
| ICEBERG_RETURN_UNEXPECTED(txn->AddUpdate(self)); |
| @@ -35,6 +35,10 @@ Status PendingUpdate::Commit() { | |||
| if (!ctx_->transaction) { | |||
| // Table-created path: no transaction exists yet, create a temporary one. | |||
| ICEBERG_ASSIGN_OR_RAISE(auto txn, Transaction::Make(ctx_)); | |||
There was a problem hiding this comment.
Please keep this temporary transaction detached. Remove ctx->transaction = ... from Transaction::Make(ctx); only explicit transactions should set it. Otherwise the next Commit() sees an expired transaction.
| } | ||
|
|
||
| std::ignore = Finalize(commit_result.value()->metadata().get()); | ||
| if (!self) { |
There was a problem hiding this comment.
Please route the empty-update branch in Transaction::Commit() through the normal finalization block. Otherwise registered no-op updates skip Finalize().
No description provided.