Repository navigation
Antalya 26.8 - Forward port of #2290 - Plain merge tree partition export - #2548
Conversation
The combined export-partition port stopped before #2290, so plain `MergeTree` had no `EXPORT PARTITION` and only `system.replicated_partition_exports`. `getExternalMetadata` returns `std::shared_ptr` on this branch, so the shared Iceberg helper uses `dynamic_pointer_cast`.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
antalya-26.8There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aaf00db011
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| std::string getCompositeKey() const | ||
| { | ||
| const auto qualified_table_name = QualifiedTableName {manifest.destination_database, manifest.destination_table}; | ||
| return manifest.partition_id + "_" + qualified_table_name.getFullName(); | ||
| return ExportPartitionUtils::compositeKey( | ||
| manifest.partition_id, manifest.destination_database, manifest.destination_table); |
There was a problem hiding this comment.
Preserve legacy Keeper keys during migration
On upgrade or in a mixed-version cluster, existing export children still use the legacy partition_id + "_" + qualified_table key, but this method now derives only the escaped dotted key. poll discovers the legacy child and inserts its manifest, while removeStaleEntries immediately compares this newly derived key against the actual legacy child name, cancels the local work, and erases the entry. The pending export is consequently stranded and a new export can be created under the new key for the same partition and destination; store the actual Keeper child key in the entry or migrate/recognize both encodings.
Useful? React with 👍 / 👎.
| if (const auto it = tasks.find(composite_key); it != tasks.end()) | ||
| { | ||
| if (!force) | ||
| throw Exception(ErrorCodes::EXPORT_PARTITION_ALREADY_EXPORTED, | ||
| "Export with key {} already exported or it is being exported. " | ||
| "Set `export_merge_tree_partition_force_export` to overwrite it.", | ||
| key_description); | ||
|
|
||
| previous_transaction_id = it->second.getDescriptor().transaction_id; | ||
| } |
There was a problem hiding this comment.
Reject force replacement while the old task is committing
If a forced export arrives after tryCommit has set committing but before its destination commit returns, this code replaces the registry entry anyway. killExportPart only cancels per-part workers, so the old Iceberg commit can still publish a snapshot under its old transaction ID; its result is then discarded because the registry contains the new ID, and the replacement task exports and commits the same files again under a different ID, duplicating rows. Handle committing like kill does—reject or defer replacement until the in-flight commit has finished.
Useful? React with 👍 / 👎.
| if (file_already_exists_policy != MergeTreePartExportFileAlreadyExistsPolicy::overwrite) | ||
| { | ||
| if (auto committed_paths = MultiFileStorageObjectStorageSink::tryReadCommittedPaths( | ||
| base_path, /* transaction_id= */ file_name, object_storage, local_context)) | ||
| { | ||
| if (committed_paths->empty()) | ||
| throw Exception(ErrorCodes::CORRUPTED_DATA, "Commit file for {} lists no data files", base_path); | ||
|
|
||
| return ImportResult::createAlreadyExported(std::move(*committed_paths)); |
There was a problem hiding this comment.
Honor the
error policy for committed parts
When a prior export's commit marker exists and export_merge_tree_part_file_already_exists_policy='error', this branch includes error among all non-overwrite policies and returns createAlreadyExported; ExportPartTask then reuses the paths and reports success. This directly contradicts the setting contract that error throws when a file already exists, preventing callers from detecting stale or conflicting output. Only skip should return createAlreadyExported, while error should raise FILE_ALREADY_EXISTS.
AGENTS.md reference: AGENTS.md:L157-L157
Useful? React with 👍 / 👎.
| void MultiFileStorageObjectStorageSink::commit() | ||
| { | ||
| /// The constructor already ruled out a pre-existing commit file for every policy but | ||
| /// `overwrite`, so seeing one here means another exporter committed this part while we wrote. | ||
| if (file_already_exists_policy != FileAlreadyExistsPolicy::overwrite | ||
| && object_storage->exists(StoredObject(commit_file_path))) | ||
| { | ||
| throw Exception(ErrorCodes::FILE_ALREADY_EXISTS, "Commit file {} already exists, aborting {} export", commit_file_path, transaction_id); | ||
| } | ||
|
|
||
| auto out = object_storage->writeObject( | ||
| StoredObject(commit_file_path), | ||
| WriteMode::Rewrite, /* attributes= */ |
There was a problem hiding this comment.
Recheck the commit marker before publishing it
For overlapping attempts of the same part—possible when a forced replacement cancels and removes an old worker while it is still writing—both attempts can observe no marker during import and begin writing. The removed commit-time existence check used to make the later finisher fail under error; now both unconditionally rewrite the marker and succeed, so the configured conflict policy is bypassed by a normal check-then-write race. Retain the policy in the sink and recheck the marker immediately before publishing it.
Useful? React with 👍 / 👎.
CI triage — @DimensionWieldrVerdict: 7 red checks, 0 caused by this PR. Every failure is pre-existing on Evidence SHA: Not PR-related
Unrelated to partition export (this PR doesn't touch
Pure infra: the job compares the PR parser against the master baseline binary, and the master
Whole-job wall-clock timeout running the SQLite logic suites, not a test assertion failure — and SQLLogic exercises none of the code this PR changes. → Infra/timeout flake. Safe to re-run.
PR health checkThe green signal is strong. The port builds on every configuration (amd debug/asan_ubsan/binary/release, arm release, wasm_parser — all 🤖 Static CI triage from praktika S3 reports + cross-PR comparison; I can't build/run ClickHouse, so correctness is ultimately validated by CI. Happy to dig into any specific check further. |
|
Ran an AI audit which returned 2 high defects that codex already mentioned as P1 issues in the earlier review: Preserve legacy Keeper keys during migration |
Both of these exist in the original PR. This is a port |
I see. In that case, I'll file them as a separate issue and ignore them for this PR. |
|
Reran CI which cleared LGTM. |
Forward port of #2290
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
Export partition on plain merge tree. Should close #1908
User facing & functional changes:
system.partition_exportstable that covers both the previousreplicated_partition_exportsand the new one for plain merge treeDocumentation entry for user-facing changes
...
CI/CD Options
Exclude tests:
Regression jobs to run: