blob: 45411184db07696f7b5a686eabcec4930711a275 [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 "paimon/core/operation/commit/conflict_detection.h"
#include <algorithm>
#include <cstddef>
#include <map>
#include <memory>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "fmt/format.h"
#include "paimon/common/data/binary_row.h"
#include "paimon/common/data/blob_utils.h"
#include "paimon/common/types/data_field.h"
#include "paimon/common/utils/binary_row_partition_computer.h"
#include "paimon/common/utils/fields_comparator.h"
#include "paimon/common/utils/range_helper.h"
#include "paimon/common/utils/vector_store_utils.h"
#include "paimon/core/deletionvectors/deletion_vectors_index_file.h"
#include "paimon/core/manifest/file_entry.h"
#include "paimon/core/manifest/file_kind.h"
#include "paimon/core/manifest/index_manifest_file.h"
#include "paimon/core/manifest/manifest_entry.h"
#include "paimon/core/manifest/manifest_file.h"
#include "paimon/core/manifest/manifest_file_meta.h"
#include "paimon/core/manifest/manifest_list.h"
#include "paimon/core/operation/commit/commit_scanner.h"
#include "paimon/core/operation/commit/manifest_entry_changes.h"
#include "paimon/core/operation/commit/row_id_column_conflict_checker.h"
#include "paimon/core/schema/table_schema.h"
#include "paimon/core/table/bucket_mode.h"
#include "paimon/core/utils/field_mapping.h"
#include "paimon/core/utils/file_store_path_factory.h"
#include "paimon/core/utils/snapshot_manager.h"
#include "paimon/utils/range.h"
#include "paimon/utils/row_range_index.h"
namespace paimon {
namespace {
bool IsDedicatedStorageFile(const std::string& file_name) {
return BlobUtils::IsBlobFile(file_name) || VectorStoreUtils::IsVectorStoreFile(file_name);
}
struct PartitionBucketKey {
BinaryRow partition;
int32_t bucket;
bool operator==(const PartitionBucketKey& other) const {
return partition == other.partition && bucket == other.bucket;
}
};
struct PartitionBucketKeyHash {
size_t operator()(const PartitionBucketKey& key) const {
return std::hash<BinaryRow>()(key.partition) ^ (std::hash<int32_t>()(key.bucket) << 1);
}
};
} // namespace
ConflictDetection::ConflictDetection(std::shared_ptr<TableSchema> table_schema,
const CoreOptions& options,
std::shared_ptr<SnapshotManager> snapshot_manager,
std::shared_ptr<ManifestList> manifest_list,
std::shared_ptr<ManifestFile> manifest_file,
std::shared_ptr<CommitScanner> commit_scanner,
const std::string& commit_user, const std::string& table_name,
const std::shared_ptr<FileStorePathFactory>& path_factory)
: table_schema_(std::move(table_schema)),
options_(options),
snapshot_manager_(std::move(snapshot_manager)),
manifest_list_(std::move(manifest_list)),
manifest_file_(std::move(manifest_file)),
commit_scanner_(std::move(commit_scanner)),
path_factory_(path_factory),
commit_user_(commit_user),
table_name_(table_name) {}
void ConflictDetection::SetRowIdCheckFromSnapshot(
const std::optional<int64_t>& row_id_check_from_snapshot) {
row_id_check_from_snapshot_ = row_id_check_from_snapshot;
}
bool ConflictDetection::HasRowIdCheckFromSnapshot() const {
return row_id_check_from_snapshot_.has_value();
}
Status ConflictDetection::CheckConflicts(
const Snapshot& latest_snapshot, const std::vector<ManifestEntry>& base_entries,
const std::vector<ManifestEntry>& delta_entries,
const std::vector<IndexManifestEntry>& delta_index_entries,
const std::optional<std::shared_ptr<RowIdColumnConflictChecker>>&
row_id_column_conflict_checker,
const Snapshot::CommitKind& commit_kind) const {
std::string base_commit_user = latest_snapshot.CommitUser();
PAIMON_RETURN_NOT_OK(CheckDeletionVectorsNotBypassed(delta_entries));
std::vector<ManifestEntry> all_entries = base_entries;
all_entries.insert(all_entries.end(), delta_entries.begin(), delta_entries.end());
PAIMON_RETURN_NOT_OK(CheckBucketKeepSame(all_entries, commit_kind, base_commit_user,
base_entries, delta_entries));
// check the delta, it is important not to delete and add the same file. Since scan
// relies on map for deduplication, this may result in the loss of this file
std::vector<ManifestEntry> merged_delta_entries;
if (Status status = FileEntry::MergeEntries(delta_entries, &merged_delta_entries);
!status.ok()) {
return Status::Invalid(
BuildConflictMessage("File deletion conflicts detected! Give up committing.",
base_commit_user, base_entries, delta_entries, status.ToString()));
}
std::vector<ManifestEntry> merged_entries;
// merge manifest entries and also check if the files we want to delete are still there
if (Status status = FileEntry::MergeEntries(all_entries, &merged_entries); !status.ok()) {
return Status::Invalid(
BuildConflictMessage("File deletion conflicts detected! Give up committing.",
base_commit_user, base_entries, delta_entries, status.ToString()));
}
PAIMON_RETURN_NOT_OK(
CheckDeleteInEntries(merged_entries, base_commit_user, base_entries, delta_entries));
PAIMON_RETURN_NOT_OK(
CheckKeyRange(merged_entries, base_commit_user, base_entries, delta_entries));
if (commit_kind != Snapshot::CommitKind::Compact()) {
PAIMON_RETURN_NOT_OK(
CheckRowIdExistence(base_entries, delta_entries, latest_snapshot.NextRowId()));
}
PAIMON_RETURN_NOT_OK(CheckRowIdRangeConflicts(commit_kind, merged_entries));
PAIMON_RETURN_NOT_OK(CheckGlobalIndexRowIdExistence(base_entries, delta_index_entries));
PAIMON_RETURN_NOT_OK(CheckForRowIdFromSnapshot(
latest_snapshot, delta_entries, delta_index_entries, row_id_column_conflict_checker));
return Status::OK();
}
bool ConflictDetection::ShouldBeOverwriteCommit(
const std::vector<ManifestEntry>& append_table_files,
const std::vector<IndexManifestEntry>& append_index_files) const {
for (const ManifestEntry& entry : append_table_files) {
if (entry.Kind() == FileKind::Delete()) {
return true;
}
}
for (const IndexManifestEntry& entry : append_index_files) {
if (entry.index_file->IndexType() == DeletionVectorsIndexFile::DELETION_VECTORS_INDEX) {
return true;
}
}
return false;
}
Status ConflictDetection::CheckDeletionVectorsNotBypassed(
const std::vector<ManifestEntry>& delta_entries) const {
if (!options_.DeletionVectorsEnabled() ||
ResolveBucketMode(options_.GetBucket(), table_schema_) != BucketMode::BUCKET_UNAWARE) {
return Status::OK();
}
for (const ManifestEntry& entry : delta_entries) {
if (entry.Kind() == FileKind::Delete()) {
return Status::NotImplemented(fmt::format(
"Committing the deletion of data file {} is not supported while deletion vectors "
"are enabled on a table without buckets: the conflict between it and a concurrent "
"commit rewriting that file's deletion vector cannot be detected yet.",
entry.File()->file_name));
}
}
return Status::OK();
}
Status ConflictDetection::CheckBucketKeepSame(
const std::vector<ManifestEntry>& all_entries, const Snapshot::CommitKind& commit_kind,
const std::string& base_commit_user, const std::vector<ManifestEntry>& base_entries,
const std::vector<ManifestEntry>& delta_entries) const {
if (commit_kind == Snapshot::CommitKind::Overwrite()) {
return Status::OK();
}
// total buckets within the same partition should remain the same
std::unordered_map<BinaryRow, int32_t> total_buckets;
for (const ManifestEntry& entry : all_entries) {
if (entry.TotalBuckets() <= 0) {
continue;
}
if (same_bucket_checked_partitions_.find(entry.Partition()) !=
same_bucket_checked_partitions_.end()) {
continue;
}
auto [iter, inserted] = total_buckets.emplace(entry.Partition(), entry.TotalBuckets());
if (inserted || iter->second == entry.TotalBuckets()) {
continue;
}
return TotalBucketsChanged(entry.Partition(), entry.TotalBuckets(), iter->second,
base_commit_user, base_entries, delta_entries);
}
MarkBucketCheckedPartitions(total_buckets);
return Status::OK();
}
Status ConflictDetection::CollectUncheckedBucketPartitions(
const std::vector<ManifestEntry>& delta_entries,
std::unordered_map<BinaryRow, int32_t>* total_buckets) const {
total_buckets->clear();
for (const ManifestEntry& entry : delta_entries) {
if (!(entry.Kind() == FileKind::Add()) || entry.TotalBuckets() <= 0 ||
same_bucket_checked_partitions_.find(entry.Partition()) !=
same_bucket_checked_partitions_.end()) {
continue;
}
auto [iter, inserted] = total_buckets->emplace(entry.Partition(), entry.TotalBuckets());
if (!inserted && iter->second != entry.TotalBuckets()) {
return BucketNumMismatch(entry.Partition(), entry.TotalBuckets(), iter->second);
}
}
return Status::OK();
}
Status ConflictDetection::CheckSameBucketByTotalBuckets(
const std::unordered_map<BinaryRow, int32_t>& expected_total_buckets,
const std::unordered_map<BinaryRow, int32_t>& previous_total_buckets) const {
for (const auto& [partition, total_buckets] : expected_total_buckets) {
auto iter = previous_total_buckets.find(partition);
if (iter != previous_total_buckets.end() && iter->second != total_buckets) {
return BucketNumMismatch(partition, total_buckets, iter->second);
}
}
MarkBucketCheckedPartitions(expected_total_buckets);
return Status::OK();
}
Status ConflictDetection::BucketNumMismatch(const BinaryRow& partition, int32_t num_buckets,
int32_t previous_num_buckets) const {
std::string part_info;
if (table_schema_->PartitionKeys().empty()) {
part_info = "table";
} else {
PAIMON_ASSIGN_OR_RAISE(std::string partition_string,
path_factory_->GetPartitionString(partition));
part_info = fmt::format("partition {{{}}}", partition_string);
}
return Status::Invalid(fmt::format(
"Try to write {} with a new bucket num {}, but the previous bucket num is {}. Please "
"switch to batch mode, and perform INSERT OVERWRITE to rescale current data layout first.",
part_info, num_buckets, previous_num_buckets));
}
Status ConflictDetection::TotalBucketsChanged(
const BinaryRow& partition, int32_t num_buckets, int32_t previous_num_buckets,
const std::string& base_commit_user, const std::vector<ManifestEntry>& base_entries,
const std::vector<ManifestEntry>& delta_entries) const {
std::shared_ptr<arrow::Schema> arrow_schema =
DataField::ConvertDataFieldsToArrowSchema(table_schema_->Fields());
PAIMON_ASSIGN_OR_RAISE(
std::shared_ptr<arrow::Schema> partition_schema,
FieldMapping::GetPartitionSchema(arrow_schema, table_schema_->PartitionKeys()));
PAIMON_ASSIGN_OR_RAISE(
std::string partition_string,
BinaryRowPartitionComputer::PartToSimpleString(partition_schema, partition, "-", 200,
/*legacy_partition_name_enabled=*/false));
std::string message = fmt::format(
"Total buckets of partition {} changed from {} to {} without overwrite. Give up "
"committing.",
partition_string, previous_num_buckets, num_buckets);
return Status::Invalid(
BuildConflictMessage(message, base_commit_user, base_entries, delta_entries));
}
std::string ConflictDetection::BuildConflictMessage(const std::string& message,
const std::string& base_commit_user,
const std::vector<ManifestEntry>& base_entries,
const std::vector<ManifestEntry>& delta_entries,
const std::string& cause) const {
static constexpr const char* kPossibleCauses =
"Don't panic!\n"
"Conflicts during commits are normal and this failure is intended to resolve the "
"conflicts.\n"
"Conflicts are mainly caused by the following scenarios:\n"
"1. Multiple jobs are writing into the same partition at the same time, or you use "
"STATEMENT SET to execute multiple INSERT statements into the same Paimon table.\n"
" You'll probably see different base commit user and current commit user below.\n"
" You can use dedicated compaction job to support multiple writing.\n"
"2. You're recovering from an old savepoint, or you're creating multiple jobs from a "
"savepoint.\n"
" The job will fail continuously in this scenario to protect metadata from corruption.\n"
" You can either recover from the latest savepoint, or you can revert the table to the "
"snapshot corresponding to the old savepoint.";
constexpr size_t kMaxEntry = 50;
auto join_entries = [](const std::vector<ManifestEntry>& entries,
size_t max_entry) -> std::string {
std::string joined;
size_t limit = std::min(entries.size(), max_entry);
for (size_t i = 0; i < limit; ++i) {
if (i > 0) {
joined += "\n";
}
joined += entries[i].ToString();
}
return joined;
};
std::string commit_user_string = fmt::format(
"Base commit user is: {}; Current commit user is: {}", base_commit_user, commit_user_);
std::string base_entries_string = "Base entries are:\n" + join_entries(base_entries, kMaxEntry);
std::string changes_string = "Changes are:\n" + join_entries(delta_entries, kMaxEntry);
std::string result = fmt::format("{}\n\n{}\n\n{}\n\n{}\n\n{}", message, kPossibleCauses,
commit_user_string, base_entries_string, changes_string);
if (base_entries.size() > kMaxEntry || delta_entries.size() > kMaxEntry) {
result +=
"\n\nThe entry list above are not fully displayed, please refer to logs for more "
"information.";
}
if (!cause.empty()) {
result += "\n\nCaused by: " + cause;
}
return result;
}
void ConflictDetection::MarkBucketCheckedPartitions(
const std::unordered_map<BinaryRow, int32_t>& total_buckets) const {
if (total_buckets.empty()) {
return;
}
for (const auto& [partition, _] : total_buckets) {
same_bucket_checked_partitions_.insert_or_assign(partition, true);
while (same_bucket_checked_partitions_.size() > kSameBucketCheckCacheMaxSize) {
same_bucket_checked_partitions_.erase(same_bucket_checked_partitions_.begin()->first);
}
}
}
Status ConflictDetection::CheckDeleteInEntries(
const std::vector<ManifestEntry>& merged_entries, const std::string& base_commit_user,
const std::vector<ManifestEntry>& base_entries,
const std::vector<ManifestEntry>& delta_entries) const {
for (const auto& entry : merged_entries) {
if (entry.Kind() == FileKind::Delete()) {
std::string message = fmt::format(
"File deletion conflicts detected! Give up committing. Trying to delete file {} "
"for table {} which is not previously added.",
entry.FileName(), table_name_);
return Status::Invalid(
BuildConflictMessage(message, base_commit_user, base_entries, delta_entries));
}
}
return Status::OK();
}
Status ConflictDetection::CheckKeyRange(const std::vector<ManifestEntry>& merged_entries,
const std::string& base_commit_user,
const std::vector<ManifestEntry>& base_entries,
const std::vector<ManifestEntry>& delta_entries) const {
if (table_schema_->PrimaryKeys().empty()) {
return Status::OK();
}
PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> trimmed_primary_key_fields,
table_schema_->TrimmedPrimaryKeyFields());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FieldsComparator> key_comparator,
FieldsComparator::Create(trimmed_primary_key_fields,
options_.SequenceFieldSortOrderIsAscending()));
// group entries by partitions, buckets and levels
std::unordered_map<std::tuple<BinaryRow, int32_t, int32_t>, std::vector<ManifestEntry>> levels;
for (const auto& entry : merged_entries) {
if (!(entry.Kind() == FileKind::Add())) {
continue;
}
int32_t level = entry.Level();
if (level < 1) {
continue;
}
levels[std::make_tuple(entry.Partition(), entry.Bucket(), level)].push_back(entry);
}
// check for all LSM level >= 1, key ranges of files do not intersect
for (auto& [_, entries] : levels) {
std::sort(entries.begin(), entries.end(),
[&key_comparator](const ManifestEntry& a, const ManifestEntry& b) {
return key_comparator->CompareTo(a.MinKey(), b.MinKey()) < 0;
});
for (size_t i = 0; i + 1 < entries.size(); ++i) {
const ManifestEntry& a = entries[i];
const ManifestEntry& b = entries[i + 1];
if (key_comparator->CompareTo(a.MaxKey(), b.MinKey()) >= 0) {
PAIMON_ASSIGN_OR_RAISE(std::string a_partition_string,
path_factory_->GetPartitionString(a.Partition()));
PAIMON_ASSIGN_OR_RAISE(std::string b_partition_string,
path_factory_->GetPartitionString(b.Partition()));
std::string message = fmt::format(
"LSM conflicts detected! Give up committing. Conflict files are:\n"
"{}, bucket {}, level {}, file {}\n"
"{}, bucket {}, level {}, file {}",
a_partition_string, a.Bucket(), a.Level(), a.FileName(), b_partition_string,
b.Bucket(), b.Level(), b.FileName());
return Status::Invalid(
BuildConflictMessage(message, base_commit_user, base_entries, delta_entries));
}
}
}
return Status::OK();
}
Status ConflictDetection::CheckRowIdExistence(const std::vector<ManifestEntry>& base_entries,
const std::vector<ManifestEntry>& delta_entries,
const std::optional<int64_t>& next_row_id) const {
if (!options_.DataEvolutionEnabled()) {
return Status::OK();
}
std::vector<ManifestEntry> files_to_check;
files_to_check.reserve(delta_entries.size());
for (const ManifestEntry& entry : delta_entries) {
if (!(entry.Kind() == FileKind::Add()) || !entry.File()->first_row_id || !next_row_id ||
entry.File()->first_row_id.value() >= next_row_id.value()) {
continue;
}
files_to_check.push_back(entry);
}
if (files_to_check.empty()) {
return Status::OK();
}
std::vector<Range> existing_data_ranges;
existing_data_ranges.reserve(base_entries.size());
for (const ManifestEntry& entry : base_entries) {
if (!entry.File()->first_row_id || IsDedicatedStorageFile(entry.FileName())) {
continue;
}
int64_t range_from = entry.File()->first_row_id.value();
int64_t range_to = range_from + entry.File()->row_count - 1;
existing_data_ranges.emplace_back(range_from, range_to);
}
PAIMON_ASSIGN_OR_RAISE(RowRangeIndex existing_index,
RowRangeIndex::Create(existing_data_ranges,
/*merge_adjacent=*/false));
for (const ManifestEntry& entry : files_to_check) {
int64_t range_from = entry.File()->first_row_id.value();
int64_t range_to = range_from + entry.File()->row_count - 1;
Range row_range(range_from, range_to);
bool exists = false;
if (IsDedicatedStorageFile(entry.FileName())) {
exists = existing_index.Contains(row_range);
} else {
exists = existing_index.ContainsExactly(row_range);
}
if (!exists) {
return Status::Invalid(fmt::format(
"Row ID existence conflict: file '{}' references firstRowId={}, rowCount={} in "
"bucket {}, but no matching file exists in the current snapshot. The referenced "
"file may have been rewritten by a concurrent compaction or removed by an "
"overwrite.",
entry.FileName(), entry.File()->first_row_id.value(), entry.File()->row_count,
entry.Bucket()));
}
}
return Status::OK();
}
Status ConflictDetection::CheckRowIdRangeConflicts(
const Snapshot::CommitKind& commit_kind,
const std::vector<ManifestEntry>& merged_entries) const {
if (!options_.DataEvolutionEnabled()) {
return Status::OK();
}
if (!row_id_check_from_snapshot_ && !(commit_kind == Snapshot::CommitKind::Compact())) {
return Status::OK();
}
std::vector<ManifestEntry> entries_with_ranges;
entries_with_ranges.reserve(merged_entries.size());
for (const ManifestEntry& entry : merged_entries) {
if (entry.File()->first_row_id) {
entries_with_ranges.push_back(entry);
}
}
if (entries_with_ranges.empty()) {
return Status::OK();
}
RangeHelper<ManifestEntry> range_helper(
[](const ManifestEntry& entry) -> Result<int64_t> {
return entry.File()->first_row_id.value();
},
[](const ManifestEntry& entry) -> Result<int64_t> {
return entry.File()->first_row_id.value() + entry.File()->row_count - 1;
});
std::vector<ManifestEntry> data_files;
std::vector<ManifestEntry> dedicated_files;
data_files.reserve(entries_with_ranges.size());
dedicated_files.reserve(entries_with_ranges.size());
for (const ManifestEntry& entry : entries_with_ranges) {
if (IsDedicatedStorageFile(entry.FileName())) {
dedicated_files.push_back(entry);
} else {
data_files.push_back(entry);
}
}
PAIMON_RETURN_NOT_OK(CheckDataFileRowIdRangeConflicts(range_helper, data_files));
PAIMON_RETURN_NOT_OK(CheckDedicatedFileRowIdRangeConflicts(data_files, dedicated_files));
return Status::OK();
}
Status ConflictDetection::CheckDataFileRowIdRangeConflicts(
RangeHelper<ManifestEntry>& range_helper, const std::vector<ManifestEntry>& data_files) const {
std::vector<ManifestEntry> data_files_copy = data_files;
PAIMON_ASSIGN_OR_RAISE(std::vector<std::vector<ManifestEntry>> data_file_groups,
range_helper.MergeOverlappingRanges(std::move(data_files_copy)));
for (const std::vector<ManifestEntry>& data_file_group : data_file_groups) {
PAIMON_ASSIGN_OR_RAISE(bool all_data_ranges_same,
range_helper.AreAllRangesSame(data_file_group));
if (!all_data_ranges_same) {
std::string data_files_str;
for (size_t i = 0; i < data_file_group.size(); ++i) {
if (i > 0) {
data_files_str += ", ";
}
data_files_str += data_file_group[i].ToString();
}
return Status::Invalid(fmt::format(
"For Data Evolution table, multiple 'MERGE INTO' and 'COMPACT' operations have "
"encountered conflicts, data files: [{}]",
data_files_str));
}
}
return Status::OK();
}
Status ConflictDetection::CheckDedicatedFileRowIdRangeConflicts(
const std::vector<ManifestEntry>& data_files,
const std::vector<ManifestEntry>& dedicated_files) const {
if (dedicated_files.empty()) {
return Status::OK();
}
std::vector<Range> data_ranges;
data_ranges.reserve(data_files.size());
for (const ManifestEntry& data_file : data_files) {
int64_t data_range_from = data_file.File()->first_row_id.value();
int64_t data_range_to = data_range_from + data_file.File()->row_count - 1;
data_ranges.emplace_back(data_range_from, data_range_to);
}
PAIMON_ASSIGN_OR_RAISE(RowRangeIndex data_file_row_range_index,
RowRangeIndex::Create(data_ranges, /*merge_adjacent=*/false));
for (const ManifestEntry& dedicated_file : dedicated_files) {
int64_t dedicated_from = dedicated_file.File()->first_row_id.value();
int64_t dedicated_to = dedicated_from + dedicated_file.File()->row_count - 1;
Range dedicated_range(dedicated_from, dedicated_to);
std::vector<Range> intersecting_ranges =
data_file_row_range_index.IntersectedRanges(dedicated_range.from, dedicated_range.to);
bool covered_by_one_data_range = intersecting_ranges.size() == 1 &&
intersecting_ranges[0].from <= dedicated_range.from &&
intersecting_ranges[0].to >= dedicated_range.to;
if (!covered_by_one_data_range) {
std::string conflict_reason = intersecting_ranges.size() > 1
? "spans multiple data file ranges"
: "is not covered by one data file range";
std::string intersecting_files_str;
bool first = true;
for (const ManifestEntry& data_file : data_files) {
int64_t data_from = data_file.File()->first_row_id.value();
int64_t data_to = data_from + data_file.File()->row_count - 1;
if (data_from <= dedicated_range.to && dedicated_range.from <= data_to) {
if (!first) {
intersecting_files_str += ", ";
}
intersecting_files_str += data_file.ToString();
first = false;
}
}
return Status::Invalid(fmt::format(
"For Data Evolution table, multiple 'MERGE INTO' and 'COMPACT' operations have "
"encountered conflicts, dedicated file {} {} {}: [{}]",
dedicated_file.ToString(), dedicated_range.ToString(), conflict_reason,
intersecting_files_str));
}
}
return Status::OK();
}
Status ConflictDetection::CheckForRowIdFromSnapshot(
const Snapshot& latest_snapshot, const std::vector<ManifestEntry>& delta_entries,
const std::vector<IndexManifestEntry>& delta_index_entries,
const std::optional<std::shared_ptr<RowIdColumnConflictChecker>>&
row_id_column_conflict_checker) const {
if (!options_.DataEvolutionEnabled() || !row_id_check_from_snapshot_ || !snapshot_manager_ ||
!row_id_column_conflict_checker || !row_id_column_conflict_checker.value() ||
row_id_column_conflict_checker.value()->IsEmpty()) {
return Status::OK();
}
if (row_id_check_from_snapshot_.value() > latest_snapshot.Id()) {
return Status::OK();
}
PAIMON_ASSIGN_OR_RAISE(Snapshot check_snapshot,
snapshot_manager_->LoadSnapshot(row_id_check_from_snapshot_.value()));
if (!check_snapshot.NextRowId()) {
return Status::Invalid(fmt::format("Next row id cannot be null for snapshot {}.",
row_id_check_from_snapshot_.value()));
}
int64_t check_next_row_id = check_snapshot.NextRowId().value();
int64_t from_snapshot_id = row_id_check_from_snapshot_.value() + 1;
if (from_snapshot_id < Snapshot::FIRST_SNAPSHOT_ID) {
from_snapshot_id = Snapshot::FIRST_SNAPSHOT_ID;
}
if (from_snapshot_id > latest_snapshot.Id()) {
return Status::OK();
}
std::vector<BinaryRow> changed_partitions =
ManifestEntryChanges::ChangedPartitions(delta_entries, delta_index_entries);
if (changed_partitions.empty()) {
return Status::OK();
}
std::unordered_set<BinaryRow> changed_partition_set(changed_partitions.begin(),
changed_partitions.end());
for (int64_t snapshot_id = from_snapshot_id; snapshot_id <= latest_snapshot.Id();
++snapshot_id) {
PAIMON_ASSIGN_OR_RAISE(Snapshot snapshot, snapshot_manager_->LoadSnapshot(snapshot_id));
if (snapshot.GetCommitKind() == Snapshot::CommitKind::Compact()) {
continue;
}
PAIMON_ASSIGN_OR_RAISE(
std::vector<ManifestEntry> history_entries,
commit_scanner_->ReadIncrementalEntries(snapshot, changed_partitions));
for (const ManifestEntry& history_entry : history_entries) {
if (!(history_entry.Kind() == FileKind::Add()) || !history_entry.File()->first_row_id ||
changed_partition_set.find(history_entry.Partition()) ==
changed_partition_set.end()) {
continue;
}
int64_t history_first_row_id = history_entry.File()->first_row_id.value();
if (history_first_row_id >= check_next_row_id) {
continue;
}
PAIMON_ASSIGN_OR_RAISE(
bool conflicts,
row_id_column_conflict_checker.value()->ConflictsWith(history_entry.File()));
if (conflicts) {
return Status::Invalid(
"For Data Evolution table, multiple 'MERGE INTO' operations have "
"encountered conflicts, updating the same file, which can render some "
"updates ineffective.");
}
}
}
return Status::OK();
}
Status ConflictDetection::CheckGlobalIndexRowIdExistence(
const std::vector<ManifestEntry>& base_entries,
const std::vector<IndexManifestEntry>& delta_index_entries) const {
if (!options_.DataEvolutionEnabled()) {
return Status::OK();
}
std::vector<IndexManifestEntry> indexes_to_check;
for (const IndexManifestEntry& index_entry : delta_index_entries) {
if (!(index_entry.kind == FileKind::Add()) ||
!index_entry.index_file->GetGlobalIndexMeta()) {
continue;
}
indexes_to_check.push_back(index_entry);
}
if (indexes_to_check.empty()) {
return Status::OK();
}
std::unordered_map<PartitionBucketKey, std::vector<Range>, PartitionBucketKeyHash>
data_ranges_by_group;
for (const ManifestEntry& base_entry : base_entries) {
if (!(base_entry.Kind() == FileKind::Add()) || !base_entry.File()->first_row_id) {
continue;
}
int64_t first_row_id = base_entry.File()->first_row_id.value();
int64_t last_row_id = first_row_id + base_entry.File()->row_count - 1;
data_ranges_by_group[{base_entry.Partition(), base_entry.Bucket()}].emplace_back(
first_row_id, last_row_id);
}
std::unordered_map<PartitionBucketKey, RowRangeIndex, PartitionBucketKeyHash>
range_index_by_group;
range_index_by_group.reserve(data_ranges_by_group.size());
for (const auto& [group, data_ranges] : data_ranges_by_group) {
PAIMON_ASSIGN_OR_RAISE(RowRangeIndex row_range_index,
RowRangeIndex::Create(data_ranges, /*merge_adjacent=*/true));
range_index_by_group.emplace(group, std::move(row_range_index));
}
for (const IndexManifestEntry& index_entry : indexes_to_check) {
PartitionBucketKey group_key{index_entry.partition, index_entry.bucket};
auto group_iter = range_index_by_group.find(group_key);
if (group_iter == range_index_by_group.end()) {
return Status::Invalid(fmt::format(
"Global index row ID existence conflict: index file '{}' references row range {}, "
"but this range is not fully covered by current data files. The referenced row "
"IDs may have been reassigned or removed by a concurrent commit.",
index_entry.index_file->FileName(),
Range(index_entry.index_file->GetGlobalIndexMeta().value().row_range_start,
index_entry.index_file->GetGlobalIndexMeta().value().row_range_end)
.ToString()));
}
const GlobalIndexMeta& global_index = index_entry.index_file->GetGlobalIndexMeta().value();
Range index_range(global_index.row_range_start, global_index.row_range_end);
std::vector<Range> intersected =
group_iter->second.IntersectedRanges(index_range.from, index_range.to);
bool covered = intersected.size() == 1 && intersected[0].from <= index_range.from &&
intersected[0].to >= index_range.to;
if (!covered) {
return Status::Invalid(fmt::format(
"Global index row ID existence conflict: index file '{}' references row range {}, "
"but this range is not fully covered by current data files. The referenced row "
"IDs may have been reassigned or removed by a concurrent commit.",
index_entry.index_file->FileName(), index_range.ToString()));
}
}
return Status::OK();
}
} // namespace paimon