blob: 129a983b1051f5097999bd05987c329042abc2cc [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "iceberg/update/update_partition_spec.h"
#include <algorithm>
#include <format>
#include <memory>
#include <string_view>
#include <utility>
#include "iceberg/expression/term.h"
#include "iceberg/partition_field.h"
#include "iceberg/partition_spec.h"
#include "iceberg/schema.h"
#include "iceberg/table_metadata.h"
#include "iceberg/transaction.h"
#include "iceberg/transform.h"
#include "iceberg/util/macros.h"
namespace iceberg {
Result<std::shared_ptr<UpdatePartitionSpec>> UpdatePartitionSpec::Make(
std::shared_ptr<TransactionContext> ctx) {
ICEBERG_PRECHECK(ctx != nullptr, "Cannot create UpdatePartitionSpec without context");
return std::shared_ptr<UpdatePartitionSpec>(new UpdatePartitionSpec(std::move(ctx)));
}
UpdatePartitionSpec::UpdatePartitionSpec(std::shared_ptr<TransactionContext> ctx)
: PendingUpdate(std::move(ctx)) {
format_version_ = base().format_version;
// Get the current/default partition spec
auto spec_result = base().PartitionSpec();
if (!spec_result.has_value()) {
AddError(spec_result.error());
return;
}
spec_ = std::move(spec_result.value());
// Get the current schema
auto schema_result = base().Schema();
if (!schema_result.has_value()) {
AddError(schema_result.error());
return;
}
schema_ = std::move(schema_result.value());
last_assigned_partition_id_ =
std::max(base().last_partition_id, PartitionSpec::kLegacyPartitionDataIdStart - 1);
name_to_field_ = IndexSpecByName(*spec_);
transform_to_field_ = IndexSpecByTransform(*spec_);
// Check for unknown transforms
for (const auto& field : spec_->fields()) {
if (field.transform()->transform_type() == TransformType::kUnknown) {
AddError(ErrorKind::kInvalidArgument,
"Cannot update partition spec with unknown transform: {}",
field.ToString());
return;
}
}
// Build index of historical partition fields for efficient recycling (V2+)
if (format_version_ >= 2) {
BuildHistoricalFieldsIndex();
}
}
UpdatePartitionSpec::~UpdatePartitionSpec() = default;
UpdatePartitionSpec& UpdatePartitionSpec::CaseSensitive(bool is_case_sensitive) {
case_sensitive_ = is_case_sensitive;
return *this;
}
UpdatePartitionSpec& UpdatePartitionSpec::AddNonDefaultSpec() {
set_as_default_ = false;
return *this;
}
UpdatePartitionSpec& UpdatePartitionSpec::AddField(std::string_view source_name) {
// Find the source field in the schema
ICEBERG_BUILDER_ASSIGN_OR_RETURN(
auto field_opt, schema_->FindFieldByName(source_name, case_sensitive_));
ICEBERG_BUILDER_CHECK(field_opt.has_value(), "Cannot find source field: {}",
source_name);
int32_t source_id = field_opt->get().field_id();
return AddFieldInternal(source_name, source_id, Transform::Identity());
}
UpdatePartitionSpec& UpdatePartitionSpec::AddField(const std::shared_ptr<Term>& term,
std::string_view part_name) {
ICEBERG_BUILDER_CHECK(term->is_unbound(), "Cannot add bound term to partition spec");
// Bind the term to get the source id
if (term->kind() == Term::Kind::kReference) {
const auto& ref = dynamic_cast<const NamedReference&>(*term);
ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto bound_ref, ref.Bind(*schema_, case_sensitive_));
int32_t source_id = bound_ref->field().field_id();
return AddFieldInternal(part_name, source_id, Transform::Identity());
} else if (term->kind() == Term::Kind::kTransform) {
const auto& unbound_transform = dynamic_cast<const UnboundTransform&>(*term);
ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto bound_transform,
unbound_transform.Bind(*schema_, case_sensitive_));
int32_t source_id = bound_transform->reference()->field().field_id();
return AddFieldInternal(part_name, source_id, bound_transform->transform());
}
AddError(InvalidArgument("Cannot add {} term to partition spec", term->ToString()));
return *this;
}
UpdatePartitionSpec& UpdatePartitionSpec::AddFieldInternal(
std::string_view name, int32_t source_id,
const std::shared_ptr<Transform>& transform) {
// Check for duplicate name in added fields
ICEBERG_BUILDER_CHECK(name.empty() || !added_field_names_.contains(name),
"Cannot add duplicate partition field: {}", name);
// Cache transform string to avoid repeated ToString() calls
const std::string transform_str = transform->ToString();
TransformKey validation_key{source_id, transform_str};
// Check if this field already exists in the current spec
auto existing_it = transform_to_field_.find(validation_key);
if (existing_it != transform_to_field_.end()) {
const auto& existing = existing_it->second;
const bool is_deleted = deletes_.contains(existing->field_id());
if (is_deleted && *existing->transform() == *transform) {
// If the field was deleted and we're re-adding the same one, just undo the delete
return RewriteDeleteAndAddField(*existing, name);
}
ICEBERG_BUILDER_CHECK(
is_deleted,
"Cannot add duplicate partition field '{}' for source {} with transform {}, "
"conflicts with {}",
name, source_id, transform_str, existing->ToString());
}
// Check if already being added
auto added_it = transform_to_added_field_.find(validation_key);
ICEBERG_BUILDER_CHECK(
added_it == transform_to_added_field_.end(),
"Cannot add duplicate partition field '{}' for source {} with transform {}, "
"already added: {}",
name, source_id, transform_str, added_it->second);
// Create or recycle the partition field
PartitionField new_field = RecycleOrCreatePartitionField(source_id, transform, name);
// Generate name if not provided
std::string field_name;
if (!name.empty()) {
field_name = name;
} else {
ICEBERG_BUILDER_ASSIGN_OR_RETURN(field_name,
GeneratePartitionName(source_id, transform));
}
// Create the final field with the name
new_field = PartitionField(new_field.source_id(), new_field.field_id(), field_name,
new_field.transform());
// Check for redundant time-based partitions
CheckForRedundantAddedPartitions(new_field);
transform_to_added_field_.emplace(validation_key, field_name);
// Handle name conflicts with existing fields
auto existing_name_it = name_to_field_.find(field_name);
if (existing_name_it != name_to_field_.end()) {
const auto& existing_field = existing_name_it->second;
const bool existing_is_deleted = deletes_.contains(existing_field->field_id());
std::string renamed =
std::format("{}_{}", existing_field->name(), existing_field->field_id());
if (!existing_is_deleted) {
if (IsVoidTransform(*existing_field)) {
// Rename the old deleted field
RenameField(existing_field->name(), std::move(renamed));
} else {
AddError(
InvalidArgument("Cannot add duplicate partition field name: {}", field_name));
return *this;
}
} else {
// Field is being deleted, rename it to avoid conflict
renames_.emplace(existing_field->name(), std::move(renamed));
}
}
adds_.push_back(std::move(new_field));
added_field_names_.emplace(field_name);
return *this;
}
UpdatePartitionSpec& UpdatePartitionSpec::RewriteDeleteAndAddField(
const PartitionField& existing, std::string_view name) {
deletes_.erase(existing.field_id());
if (name.empty() || std::string(existing.name()) == name) {
return *this;
}
return RenameField(std::string(existing.name()), std::string{name});
}
UpdatePartitionSpec& UpdatePartitionSpec::RemoveField(std::string_view name) {
// Cannot delete newly added fields
ICEBERG_BUILDER_CHECK(!added_field_names_.contains(name),
"Cannot delete newly added field: {}", name);
// Cannot rename and delete
ICEBERG_BUILDER_CHECK(!renames_.contains(name),
"Cannot rename and delete partition field: {}", name);
auto field_it = name_to_field_.find(name);
ICEBERG_BUILDER_CHECK(field_it != name_to_field_.end(),
"Cannot find partition field to remove: {}", name);
deletes_.insert(field_it->second->field_id());
return *this;
}
UpdatePartitionSpec& UpdatePartitionSpec::RemoveField(const std::shared_ptr<Term>& term) {
ICEBERG_BUILDER_CHECK(term->is_unbound(),
"Cannot remove bound term from partition spec");
// Bind the term to get the source id
if (term->kind() == Term::Kind::kReference) {
const auto& ref = dynamic_cast<const NamedReference&>(*term);
ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto bound_ref, ref.Bind(*schema_, case_sensitive_));
int32_t source_id = bound_ref->field().field_id();
// Reference terms use identity transform
TransformKey key{source_id, Transform::Identity()->ToString()};
return RemoveFieldByTransform(key, term->ToString());
} else if (term->kind() == Term::Kind::kTransform) {
const auto& unbound_transform = dynamic_cast<const UnboundTransform&>(*term);
ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto bound_transform,
unbound_transform.Bind(*schema_, case_sensitive_));
int32_t source_id = bound_transform->reference()->field().field_id();
auto transform = bound_transform->transform();
TransformKey key{source_id, transform->ToString()};
return RemoveFieldByTransform(key, term->ToString());
}
AddError(
InvalidArgument("Cannot remove {} term from partition spec", term->ToString()));
return *this;
}
UpdatePartitionSpec& UpdatePartitionSpec::RemoveFieldByTransform(
const TransformKey& key, std::string_view term_str) {
// Cannot delete newly added fields
ICEBERG_BUILDER_CHECK(!transform_to_added_field_.contains(key),
"Cannot delete newly added field: {}", term_str);
auto field_it = transform_to_field_.find(key);
ICEBERG_BUILDER_CHECK(field_it != transform_to_field_.end(),
"Cannot find partition field to remove: {}", term_str);
const auto& field = field_it->second;
// Cannot rename and delete
ICEBERG_BUILDER_CHECK(!renames_.contains(std::string(field->name())),
"Cannot rename and delete partition field: {}", field->name());
deletes_.insert(field->field_id());
return *this;
}
UpdatePartitionSpec& UpdatePartitionSpec::RenameField(std::string_view name,
std::string new_name) {
// Handle existing void field with the new name
auto existing_it = name_to_field_.find(new_name);
if (existing_it != name_to_field_.end() && IsVoidTransform(*existing_it->second)) {
std::string renamed = std::format("{}_{}", existing_it->second->name(),
existing_it->second->field_id());
RenameField(existing_it->second->name(), std::move(renamed));
}
// Cannot rename newly added fields
ICEBERG_BUILDER_CHECK(!added_field_names_.contains(name),
"Cannot rename newly added partition field: {}", name);
auto field_it = name_to_field_.find(name);
ICEBERG_BUILDER_CHECK(field_it != name_to_field_.end(),
"Cannot find partition field to rename: {}", name);
// Cannot delete and rename
ICEBERG_BUILDER_CHECK(!deletes_.contains(field_it->second->field_id()),
"Cannot delete and rename partition field: {}", name);
renames_.emplace(name, std::move(new_name));
return *this;
}
Result<UpdatePartitionSpec::ApplyResult> UpdatePartitionSpec::Apply() {
ICEBERG_RETURN_UNEXPECTED(CheckErrors());
std::vector<PartitionField> new_fields;
new_fields.reserve(spec_->fields().size() + adds_.size());
// Process existing fields
for (const auto& field : spec_->fields()) {
if (!deletes_.contains(field.field_id())) {
// Field is kept, check for rename
auto rename_it = renames_.find(field.name());
if (rename_it != renames_.end()) {
new_fields.emplace_back(field.source_id(), field.field_id(), rename_it->second,
field.transform());
} else {
new_fields.push_back(field);
}
} else if (format_version_ < 2) {
// field IDs were not required for v1 and were assigned sequentially in each
// partition spec starting at 1,000.
// To maintain consistent field ids across partition specs in v1 tables, any
// partition field that is removed must be replaced with a null transform. null
// values are always allowed in partition data.
auto rename_it = renames_.find(field.name());
std::string field_name =
rename_it != renames_.end() ? rename_it->second : std::string(field.name());
new_fields.emplace_back(field.source_id(), field.field_id(), std::move(field_name),
Transform::Void());
}
// In V2, deleted fields are simply removed
}
// Add new fields
new_fields.insert(new_fields.end(), adds_.begin(), adds_.end());
// Use -1 as a placeholder for the spec id, the actual spec id will be assigned by
// TableMetadataBuilder when the AddPartitionSpec update is applied.
ICEBERG_ASSIGN_OR_RAISE(auto new_spec,
PartitionSpec::Make(/*spec_id=*/-1, std::move(new_fields)));
ICEBERG_RETURN_UNEXPECTED(new_spec->Validate(*schema_, /*allow_missing_fields=*/false));
return ApplyResult{.spec = std::move(new_spec), .set_as_default = set_as_default_};
}
int32_t UpdatePartitionSpec::AssignFieldId() { return ++last_assigned_partition_id_; }
PartitionField UpdatePartitionSpec::RecycleOrCreatePartitionField(
int32_t source_id, std::shared_ptr<Transform> transform, std::string_view name) {
// In V2+, use pre-built index for O(1) lookup instead of O(n*m) iteration
if (format_version_ >= 2 && !historical_fields_.empty()) {
auto it = historical_fields_.find(TransformKey{source_id, transform->ToString()});
if (it != historical_fields_.end()) {
const auto& field = it->second;
// If target name is specified then consider it too, otherwise not
if (name.empty() || field.name() == name) {
return field;
}
}
}
// No matching field found, create a new one
return {source_id, AssignFieldId(), std::string{name}, std::move(transform)};
}
Result<std::string> UpdatePartitionSpec::GeneratePartitionName(
int32_t source_id, const std::shared_ptr<Transform>& transform) const {
// Find the source field name
ICEBERG_ASSIGN_OR_RAISE(auto field_opt, schema_->FindFieldById(source_id));
if (!field_opt.has_value()) {
return Invalid("Cannot find source field for partition field: {}", source_id);
}
return transform->GeneratePartitionName(field_opt.value().get().name());
}
bool UpdatePartitionSpec::IsTimeTransform(const std::shared_ptr<Transform>& transform) {
switch (transform->transform_type()) {
case TransformType::kYear:
case TransformType::kMonth:
case TransformType::kDay:
case TransformType::kHour:
return true;
default:
return false;
}
}
bool UpdatePartitionSpec::IsVoidTransform(const PartitionField& field) {
return field.transform()->transform_type() == TransformType::kVoid;
}
void UpdatePartitionSpec::CheckForRedundantAddedPartitions(const PartitionField& field) {
if (IsTimeTransform(field.transform())) {
if (added_time_fields_.contains(field.source_id())) {
AddError(ErrorKind::kInvalidArgument,
"Cannot add redundant partition field: {} conflicts with {}",
field.ToString(), added_time_fields_.at(field.source_id()));
return;
}
added_time_fields_.emplace(field.source_id(), field.ToString());
}
}
std::unordered_map<std::string, const PartitionField*, StringHash, StringEqual>
UpdatePartitionSpec::IndexSpecByName(const PartitionSpec& spec) {
std::unordered_map<std::string, const PartitionField*, StringHash, StringEqual> index;
for (const auto& field : spec.fields()) {
index.emplace(field.name(), &field);
}
return index;
}
std::unordered_map<UpdatePartitionSpec::TransformKey, const PartitionField*,
UpdatePartitionSpec::TransformKeyHash>
UpdatePartitionSpec::IndexSpecByTransform(const PartitionSpec& spec) {
std::unordered_map<TransformKey, const PartitionField*, TransformKeyHash> index;
index.reserve(spec.fields().size());
for (const auto& field : spec.fields()) {
TransformKey key{field.source_id(), field.transform()->ToString()};
index.emplace(key, &field);
}
return index;
}
void UpdatePartitionSpec::BuildHistoricalFieldsIndex() {
// Count total fields across all specs to reserve capacity
size_t total_fields = 0;
for (const auto& partition_spec : base().partition_specs) {
total_fields += partition_spec->fields().size();
}
historical_fields_.reserve(total_fields);
// Index all fields from all historical partition specs
// Later specs override earlier ones for the same (source_id, transform) key
for (const auto& partition_spec : base().partition_specs) {
for (const auto& field : partition_spec->fields()) {
TransformKey key{field.source_id(), field.transform()->ToString()};
historical_fields_.emplace(key, field);
}
}
}
} // namespace iceberg