Skip to content

Commit 12dc230

Browse files
committed
feat: add ReplacePartitions core class (PR1)
Adds iceberg::ReplacePartitions — the dynamic partition overwrite operation. Each AddFile() registers the file's partition for replacement of all existing data and delete files in that partition. Produces an overwrite snapshot with "replace-partitions=true" in the summary. Unpartitioned tables replace all existing data files. Extends MergingSnapshotUpdate (matching Java's BaseReplacePartitions) so the full data+delete manifest pipeline, custom-summary-property handling, and conflict-validation helpers are inherited. AddFile() unconditionally calls DropPartition(spec_id, file->partition) — for unpartitioned specs the partition value is empty and the filter manager matches every file in that spec, so no separate AlwaysTrue path is needed. Touched partitions are tracked in a PartitionSet; Validate() uses the partition-scoped overloads of ValidateAddedDataFiles / ValidateNoNewDeleteFiles, or skips entirely when no partitions were staged. Changes: * iceberg::ReplacePartitions extending MergingSnapshotUpdate with builder API (AddFile, ValidateAppendOnly, ValidateFromSnapshot, ValidateNoConflictingData, ValidateNoConflictingDeletes) and Validate() override. * SnapshotSummaryFields::kReplacePartitions = "replace-partitions". * MergingSnapshotUpdate::SetSummaryProperty promoted from private to protected so subclasses can stash custom summary entries that survive commit retry via the cached-rebuild path. * Forward declaration in type_fwd.h. * CMake + Meson source registration. Public API wiring (Table::NewReplacePartitions(), Transaction::NewReplacePartitions()) and end-to-end tests are deferred to PR2. Tracking: #775 Related: #637
1 parent ed051a7 commit 12dc230

7 files changed

Lines changed: 251 additions & 2 deletions

File tree

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ set(ICEBERG_SOURCES
104104
update/expire_snapshots.cc
105105
update/fast_append.cc
106106
update/merge_append.cc
107+
update/replace_partitions.cc
107108
update/merging_snapshot_update.cc
108109
update/pending_update.cc
109110
update/row_delta.cc

src/iceberg/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ iceberg_sources = files(
131131
'update/merge_append.cc',
132132
'update/merging_snapshot_update.cc',
133133
'update/pending_update.cc',
134+
'update/replace_partitions.cc',
134135
'update/row_delta.cc',
135136
'update/set_snapshot.cc',
136137
'update/snapshot_manager.cc',

src/iceberg/snapshot.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ struct ICEBERG_EXPORT SnapshotSummaryFields {
251251
inline static const std::string kEngineName = "engine-name";
252252
/// \brief Version of the engine that created the snapshot
253253
inline static const std::string kEngineVersion = "engine-version";
254+
/// \brief Whether this is a replace-partitions operation
255+
inline static const std::string kReplacePartitions = "replace-partitions";
254256
};
255257

256258
/// \brief Helper class for building snapshot summaries.

src/iceberg/type_fwd.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ class TransactionContext;
241241
class DeleteFiles;
242242
class ExpireSnapshots;
243243
class FastAppend;
244+
class ReplacePartitions;
244245
class MergeAppend;
245246
class PendingUpdate;
246247
class RowDelta;

src/iceberg/update/merging_snapshot_update.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ class ICEBERG_EXPORT MergingSnapshotUpdate : public SnapshotUpdate {
296296
const std::shared_ptr<Snapshot>& parent,
297297
std::shared_ptr<FileIO> io) const;
298298

299+
/// \brief Record a caller-supplied summary entry that survives commit retry.
300+
///
301+
/// MergingSnapshotUpdate clears summary_ at the start of every Apply() and
302+
/// rebuilds it from the cached sub-builders, so subclasses must route any
303+
/// custom property through this hook rather than calling summary_.Set()
304+
/// directly. Stored entries are re-merged in Summary().
305+
void SetSummaryProperty(const std::string& property, const std::string& value) override;
306+
299307
private:
300308
struct PendingDeleteFile {
301309
std::shared_ptr<DataFile> file;
@@ -334,8 +342,6 @@ class ICEBERG_EXPORT MergingSnapshotUpdate : public SnapshotUpdate {
334342

335343
Status ManagersReady() const;
336344

337-
void SetSummaryProperty(const std::string& property, const std::string& value) override;
338-
339345
Result<std::vector<PendingDeleteFile>> MergeDVs() const;
340346

341347
/// \brief Write new data manifests for staged data files; caches the result.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/update/replace_partitions.h"
21+
22+
#include "iceberg/expression/expressions.h"
23+
#include "iceberg/partition_spec.h"
24+
#include "iceberg/snapshot.h"
25+
#include "iceberg/table.h" // IWYU pragma: keep
26+
#include "iceberg/table_metadata.h"
27+
#include "iceberg/transaction.h"
28+
#include "iceberg/util/error_collector.h"
29+
#include "iceberg/util/macros.h"
30+
31+
namespace iceberg {
32+
33+
Result<std::unique_ptr<ReplacePartitions>> ReplacePartitions::Make(
34+
std::string table_name, std::shared_ptr<TransactionContext> ctx) {
35+
ICEBERG_PRECHECK(!table_name.empty(), "Table name cannot be empty");
36+
ICEBERG_PRECHECK(ctx != nullptr, "Cannot create ReplacePartitions without a context");
37+
return std::unique_ptr<ReplacePartitions>(
38+
new ReplacePartitions(std::move(table_name), std::move(ctx)));
39+
}
40+
41+
ReplacePartitions::ReplacePartitions(std::string table_name,
42+
std::shared_ptr<TransactionContext> ctx)
43+
: MergingSnapshotUpdate(std::move(table_name), std::move(ctx)) {
44+
SetSummaryProperty(SnapshotSummaryFields::kReplacePartitions, "true");
45+
}
46+
47+
ReplacePartitions& ReplacePartitions::AddFile(const std::shared_ptr<DataFile>& file) {
48+
ICEBERG_BUILDER_CHECK(file != nullptr, "Invalid data file: null");
49+
ICEBERG_BUILDER_CHECK(file->partition_spec_id.has_value(),
50+
"Data file must have partition spec ID");
51+
52+
int32_t spec_id = file->partition_spec_id.value();
53+
ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto spec, base().PartitionSpecById(spec_id));
54+
55+
ICEBERG_BUILDER_RETURN_IF_ERROR(AddDataFile(file));
56+
// DropPartition(spec_id, partition) registers the (spec_id, partition_values)
57+
// tuple with both data and delete filter managers. For an unpartitioned spec
58+
// the partition values are empty and naturally match every file under that
59+
// spec — no separate AlwaysTrue path is needed, and validation stays scoped
60+
// to the spec rather than the whole table.
61+
ICEBERG_BUILDER_RETURN_IF_ERROR(DropPartition(spec_id, file->partition));
62+
replaced_partitions_.add(spec_id, file->partition);
63+
return *this;
64+
}
65+
66+
ReplacePartitions& ReplacePartitions::ValidateAppendOnly() {
67+
FailAnyDelete();
68+
return *this;
69+
}
70+
71+
ReplacePartitions& ReplacePartitions::ValidateFromSnapshot(int64_t snapshot_id) {
72+
starting_snapshot_id_ = snapshot_id;
73+
return *this;
74+
}
75+
76+
ReplacePartitions& ReplacePartitions::ValidateNoConflictingData() {
77+
validate_conflicting_data_ = true;
78+
return *this;
79+
}
80+
81+
ReplacePartitions& ReplacePartitions::ValidateNoConflictingDeletes() {
82+
validate_conflicting_deletes_ = true;
83+
return *this;
84+
}
85+
86+
std::string ReplacePartitions::operation() { return DataOperation::kOverwrite; }
87+
88+
Status ReplacePartitions::Validate(const TableMetadata& current_metadata,
89+
const std::shared_ptr<Snapshot>& snapshot) {
90+
if (snapshot == nullptr) {
91+
return {};
92+
}
93+
// No-op update: no partitions were staged, so there is nothing to conflict
94+
// with. Calling the validators with AlwaysTrue here would turn an empty
95+
// builder into a full-table conflict check.
96+
if (replaced_partitions_.empty()) {
97+
return {};
98+
}
99+
100+
auto io = ctx_->table->io();
101+
if (validate_conflicting_data_) {
102+
ICEBERG_RETURN_UNEXPECTED(ValidateAddedDataFiles(
103+
current_metadata, starting_snapshot_id_, replaced_partitions_, snapshot, io));
104+
}
105+
if (validate_conflicting_deletes_) {
106+
ICEBERG_RETURN_UNEXPECTED(ValidateNoNewDeleteFiles(
107+
current_metadata, starting_snapshot_id_, replaced_partitions_, snapshot, io));
108+
}
109+
return {};
110+
}
111+
112+
} // namespace iceberg
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
/// \file iceberg/update/replace_partitions.h
23+
24+
#include <cstdint>
25+
#include <memory>
26+
#include <optional>
27+
#include <string>
28+
29+
#include "iceberg/iceberg_export.h"
30+
#include "iceberg/result.h"
31+
#include "iceberg/type_fwd.h"
32+
#include "iceberg/update/merging_snapshot_update.h"
33+
#include "iceberg/util/partition_value_util.h"
34+
35+
namespace iceberg {
36+
37+
/// \brief Replaces partitions in a table with new data files.
38+
///
39+
/// ReplacePartitions dynamically identifies which partitions to overwrite based
40+
/// on the data files added via AddFile(). All existing data files in each
41+
/// touched partition are marked DELETED, and the new files are written as the
42+
/// sole data in those partitions. Partitions not referenced by any added file
43+
/// are left unchanged.
44+
///
45+
/// This operation produces a snapshot with operation="overwrite" and
46+
/// "replace-partitions"="true" in the summary. For unpartitioned tables, all
47+
/// existing files are replaced.
48+
///
49+
/// When committing, these changes are applied to the latest table snapshot.
50+
/// Commit conflicts are resolved by re-applying to the new latest snapshot
51+
/// and reattempting the commit.
52+
class ICEBERG_EXPORT ReplacePartitions : public MergingSnapshotUpdate {
53+
public:
54+
/// \brief Create a new ReplacePartitions instance.
55+
///
56+
/// \param table_name The name of the table
57+
/// \param ctx The transaction context
58+
/// \return A Result containing the ReplacePartitions instance or an error
59+
static Result<std::unique_ptr<ReplacePartitions>> Make(
60+
std::string table_name, std::shared_ptr<TransactionContext> ctx);
61+
62+
/// \brief Add a data file and mark its partition for replacement.
63+
///
64+
/// Each call registers the file's partition so all existing data files in
65+
/// that partition are replaced. Duplicate files (same path) are ignored.
66+
///
67+
/// \param file The data file to add (must have partition_spec_id set)
68+
/// \return Reference to this for method chaining
69+
ReplacePartitions& AddFile(const std::shared_ptr<DataFile>& file);
70+
71+
/// \brief Fail the commit if any existing data file would be deleted.
72+
///
73+
/// This validation is useful to ensure the operation is only applied to
74+
/// tables where no data currently exists in the affected partitions.
75+
///
76+
/// \return Reference to this for method chaining
77+
ReplacePartitions& ValidateAppendOnly();
78+
79+
/// \brief Set the snapshot ID used as the baseline for conflict validation.
80+
///
81+
/// Validations check changes that occurred after this snapshot ID. If not
82+
/// set, all ancestor snapshots through the initial snapshot are validated.
83+
///
84+
/// \param snapshot_id A snapshot ID
85+
/// \return Reference to this for method chaining
86+
ReplacePartitions& ValidateFromSnapshot(int64_t snapshot_id);
87+
88+
/// \brief Enable validation that no conflicting data files were added concurrently.
89+
///
90+
/// Fails the commit if a concurrent operation added a data file in any of
91+
/// the partitions being replaced after the snapshot set by
92+
/// ValidateFromSnapshot().
93+
///
94+
/// \return Reference to this for method chaining
95+
ReplacePartitions& ValidateNoConflictingData();
96+
97+
/// \brief Enable validation that no conflicting delete files were added concurrently.
98+
///
99+
/// Fails the commit if a concurrent operation added a delete file covering
100+
/// any of the partitions being replaced after the snapshot set by
101+
/// ValidateFromSnapshot().
102+
///
103+
/// \return Reference to this for method chaining
104+
ReplacePartitions& ValidateNoConflictingDeletes();
105+
106+
std::string operation() override;
107+
108+
protected:
109+
Status Validate(const TableMetadata& current_metadata,
110+
const std::shared_ptr<Snapshot>& snapshot) override;
111+
112+
private:
113+
explicit ReplacePartitions(std::string table_name,
114+
std::shared_ptr<TransactionContext> ctx);
115+
116+
std::optional<int64_t> starting_snapshot_id_;
117+
bool validate_conflicting_data_{false};
118+
bool validate_conflicting_deletes_{false};
119+
// Partitions touched by AddFile(); used to scope conflict validation to the
120+
// overwritten partitions instead of the whole table. For unpartitioned specs
121+
// the partition values are empty, and DropPartition(spec_id, {}) already
122+
// matches every file in that spec — no separate "whole table" path is needed.
123+
PartitionSet replaced_partitions_;
124+
};
125+
126+
} // namespace iceberg

0 commit comments

Comments
 (0)