This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 0476932 feat: add file store commit and manifest merger support (#95)
0476932 is described below
commit 04769321b45f31d0d1bca29d8e459a6d5af5281a
Author: Yonghao Fang <[email protected]>
AuthorDate: Mon Jun 22 17:31:14 2026 +0800
feat: add file store commit and manifest merger support (#95)
---
include/paimon/commit_context.h | 160 ++++
include/paimon/file_store_commit.h | 147 +++
src/paimon/core/operation/commit_context.cpp | 135 +++
src/paimon/core/operation/file_store_commit.cpp | 140 +++
.../core/operation/file_store_commit_impl.cpp | 1002 ++++++++++++++++++++
src/paimon/core/operation/file_store_commit_impl.h | 236 +++++
.../core/operation/file_store_commit_test.cpp | 78 ++
src/paimon/core/operation/manifest_file_merger.cpp | 185 ++++
src/paimon/core/operation/manifest_file_merger.h | 71 ++
.../core/operation/manifest_file_merger_test.cpp | 375 ++++++++
src/paimon/core/operation/metrics/commit_metrics.h | 48 +
.../core/operation/metrics/commit_metrics_test.cpp | 52 +
12 files changed, 2629 insertions(+)
diff --git a/include/paimon/commit_context.h b/include/paimon/commit_context.h
new file mode 100644
index 0000000..b273048
--- /dev/null
+++ b/include/paimon/commit_context.h
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "paimon/result.h"
+#include "paimon/type_fwd.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class Executor;
+class MemoryPool;
+
+/// `CommitContext` is some configuration for commit operations.
+///
+/// Please do not use this class directly, use `CommitContextBuilder` to build
a `CommitContext`
+/// which has input validation.
+/// @see CommitContextBuilder
+class PAIMON_EXPORT CommitContext {
+ public:
+ CommitContext(const std::string& root_path, const std::string& commit_user,
+ bool ignore_empty_commit, bool use_rest_catalog_commit,
+ const std::shared_ptr<MemoryPool>& memory_pool,
+ const std::shared_ptr<Executor>& executor,
+ const std::shared_ptr<FileSystem>& specific_file_system,
+ const std::map<std::string, std::string>& options);
+ ~CommitContext();
+
+ const std::string& GetRootPath() const {
+ return root_path_;
+ }
+
+ const std::string& GetCommitUser() const {
+ return commit_user_;
+ }
+
+ bool IgnoreEmptyCommit() const {
+ return ignore_empty_commit_;
+ }
+
+ bool UseRESTCatalogCommit() const {
+ return use_rest_catalog_commit_;
+ }
+
+ std::shared_ptr<MemoryPool> GetMemoryPool() const {
+ return memory_pool_;
+ }
+
+ std::shared_ptr<Executor> GetExecutor() const {
+ return executor_;
+ }
+
+ std::shared_ptr<FileSystem> GetSpecificFileSystem() const {
+ return specific_file_system_;
+ }
+
+ const std::map<std::string, std::string>& GetOptions() const {
+ return options_;
+ }
+
+ private:
+ std::string root_path_;
+ std::string commit_user_;
+ bool ignore_empty_commit_;
+ bool use_rest_catalog_commit_;
+ std::shared_ptr<MemoryPool> memory_pool_;
+ std::shared_ptr<Executor> executor_;
+ std::shared_ptr<FileSystem> specific_file_system_;
+ std::map<std::string, std::string> options_;
+};
+
+/// `CommitContextBuilder` used to build a `CommitContext`, has input
validation.
+class PAIMON_EXPORT CommitContextBuilder {
+ public:
+ /// Constructs a `CommitContextBuilder` with required parameters.
+ /// @param root_path The root path of the Paimon table.
+ /// @param commit_user The user identifier for the commit operation.
+ CommitContextBuilder(const std::string& root_path, const std::string&
commit_user);
+
+ ~CommitContextBuilder();
+
+ /// Set a configuration options map to set some option entries which are
not defined in the
+ /// table schema or whose values you want to overwrite.
+ /// @note The options map will clear the options added by `AddOption()`
before.
+ /// @param options The configuration options map.
+ /// @return Reference to this builder for method chaining.
+ CommitContextBuilder& SetOptions(const std::map<std::string, std::string>&
options);
+
+ /// Add a single configuration option which is not defined in the table
schema or whose value
+ /// you want to overwrite.
+ ///
+ /// If you want to add multiple options, call `AddOption()` multiple times
or use `SetOptions()`
+ /// instead.
+ /// @param key The option key.
+ /// @param value The option value.
+ /// @return Reference to this builder for method chaining.
+ CommitContextBuilder& AddOption(const std::string& key, const std::string&
value);
+
+ /// Sets whether to ignore empty commits (default is true).
+ /// When set to true, commits that don't contain any actual data changes
will be ignored.
+ /// @param ignore_empty_commit True to ignore empty commits, false
otherwise.
+ /// @return Reference to this builder for method chaining.
+ CommitContextBuilder& IgnoreEmptyCommit(bool ignore_empty_commit);
+
+ /// Sets whether to use REST catalog commit (default is false).
+ /// @note Temporary interface, will be removed in the future.
+ /// @param use_rest_catalog_commit True to use REST catalog commit, false
otherwise.
+ /// @return Reference to this builder for method chaining.
+ CommitContextBuilder& UseRESTCatalogCommit(bool use_rest_catalog_commit);
+
+ /// Sets the memory pool to be used for memory allocation during commit
operations.
+ /// @param memory_pool Shared pointer to the memory pool instance.
+ /// @return Reference to this builder for method chaining.
+ CommitContextBuilder& WithMemoryPool(const std::shared_ptr<MemoryPool>&
memory_pool);
+
+ /// Sets the executor to be used for asynchronous operations during commit.
+ /// @param executor Shared pointer to the executor instance.
+ /// @return Reference to this builder for method chaining.
+ CommitContextBuilder& WithExecutor(const std::shared_ptr<Executor>&
executor);
+
+ /// Sets a custom file system instance to be used for all file operations
in this commit
+ /// context.
+ /// This bypasses the global file system registry and uses the provided
implementation directly.
+ ///
+ /// @param file_system The file system to use.
+ /// @return Reference to this builder for method chaining.
+ /// @note If not set, use default file system (configured in
`Options::FILE_SYSTEM`)
+ CommitContextBuilder& WithFileSystem(const std::shared_ptr<FileSystem>&
file_system);
+
+ /// Build and return a `CommitContext` instance with input validation.
+ /// @return Result containing the constructed `CommitContext` or an error
status.
+ Result<std::unique_ptr<CommitContext>> Finish();
+
+ private:
+ class Impl;
+
+ std::unique_ptr<Impl> impl_;
+};
+
+} // namespace paimon
diff --git a/include/paimon/file_store_commit.h
b/include/paimon/file_store_commit.h
new file mode 100644
index 0000000..a702c41
--- /dev/null
+++ b/include/paimon/file_store_commit.h
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "paimon/defs.h"
+#include "paimon/executor.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/metrics.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/type_fwd.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+class CommitContext;
+class CommitMessage;
+
+/// Interface for commit operations in a file store.
+///
+/// The `FileStoreCommit` class provides interfaces for committing changes,
expiring old snapshots,
+/// dropping partitions, and retrieving commit metrics.
+class PAIMON_EXPORT FileStoreCommit {
+ public:
+ /// Create an instance of `FileStoreCommit`.
+ ///
+ /// @param context A unique pointer to the `CommitContext` used for commit
operations.
+ ///
+ /// @return A Result containing a unique pointer to the `FileStoreCommit`
instance.
+ static Result<std::unique_ptr<FileStoreCommit>>
Create(std::unique_ptr<CommitContext> context);
+
+ virtual ~FileStoreCommit() = default;
+
+ /// Commit changes to the file store.
+ ///
+ /// @param commit_messages A vector of commit messages to be committed.
+ /// @param commit_identifier An optional identifier for the commit
operation. Default is
+ /// `BATCH_WRITE_COMMIT_IDENTIFIER`.
+ /// @param watermark An optional event-time watermark used to indicate the
progress of data
+ /// processing. Default is std::nullopt.
+ /// @return Status indicating the success or failure of the commit
operation.
+ virtual Status Commit(const std::vector<std::shared_ptr<CommitMessage>>&
commit_messages,
+ int64_t commit_identifier =
BATCH_WRITE_COMMIT_IDENTIFIER,
+ std::optional<int64_t> watermark = std::nullopt) = 0;
+
+ /// Filter out all `std::vector<CommitMessage>` which have been committed
and commit the
+ /// remaining ones.
+ ///
+ /// Compared to commit, this method will first check if a
commit_identifier has been
+ /// committed, so this method might be slower. A common usage of this
method is to retry the
+ /// commit process after a failure.
+ ///
+ /// @param commit_identifier_and_messages A map containing all {@link
CommitMessage}s in
+ /// question. The key is the commit_identifier.
+ ///
+ /// @param watermark An optional event-time watermark used to indicate the
progress of data
+ /// processing. Default is std::nullopt.
+ /// @return Number of `std::vector<CommitMessage>` committed.
+ virtual Result<int32_t> FilterAndCommit(
+ const std::map<int64_t, std::vector<std::shared_ptr<CommitMessage>>>&
+ commit_identifier_and_messages,
+ std::optional<int64_t> watermark = std::nullopt) = 0;
+
+ /// Overwrite from manifest committable and partition.
+ ///
+ /// @param partitions A single partition maps each partition key to a
partition value. Depending
+ /// on the user-defined statement, the partition might not include all
partition keys. Also
+ /// note that this partition does not necessarily equal to the
partitions of the newly added
+ /// key-values. This is just the partition to be cleaned up.
+ /// @param commit_messages Description of the commit messages.
+ /// @param commit_identifier Unique identifier.
+ /// @param watermark An optional event-time watermark used to indicate the
progress of data
+ /// processing. Default is std::nullopt.
+ /// @return Result of the operation.
+ virtual Status Overwrite(const std::vector<std::map<std::string,
std::string>>& partitions,
+ const
std::vector<std::shared_ptr<CommitMessage>>& commit_messages,
+ int64_t commit_identifier,
+ std::optional<int64_t> watermark = std::nullopt)
= 0;
+
+ /// This is a temporary interface for internal use. It will be removed in
a future version.
+ /// Please do not rely on it for long-term use.
+ ///
+ /// @param partitions Description of the partitions.
+ /// @param commit_messages Description of the commit messages.
+ /// @param commit_identifier Unique identifier.
+ /// @param watermark An optional event-time watermark used to indicate the
progress of data
+ /// processing. Default is std::nullopt.
+ /// @return Result of the operation.
+ virtual Result<int32_t> FilterAndOverwrite(
+ const std::vector<std::map<std::string, std::string>>& partitions,
+ const std::vector<std::shared_ptr<CommitMessage>>& commit_messages,
+ int64_t commit_identifier, std::optional<int64_t> watermark =
std::nullopt) = 0;
+
+ /// If user want to use REST catalog commit, please set
+ /// `CommitContextBuilder::UseRESTCatalogCommit()`, then call `Commit()`
(or
+ /// `FilterAndCommit()`) normally, then call this method to get the last
commit table request,
+ /// which is a JSON string that can be used to send to REST catalog server.
+ ///
+ /// @note Temporary interface for internal use, will be removed in the
future.
+ ///
+ /// @return A Result containing a JSON string which including `snapshot`
and `statistics`, but
+ /// excluding `tableId`.
+ virtual Result<std::string> GetLastCommitTableRequest() = 0;
+
+ /// Expire old snapshot in the file store.
+ ///
+ /// @return Result<int32_t> indicating the number of expired items or an
error status.
+ virtual Result<int32_t> Expire() = 0;
+
+ /// Drop specified partitions from the file store.
+ ///
+ /// @param partitions A vector of partitions to be dropped.
+ /// @param commit_identifier An identifier for the commit operation.
+ /// @return Status indicating the success or failure of the drop partition
operation.
+ virtual Status DropPartition(const std::vector<std::map<std::string,
std::string>>& partitions,
+ int64_t commit_identifier) = 0;
+
+ /// Retrieve metrics related to commit operations.
+ ///
+ /// @return A shared pointer to a `Metrics` object containing commit
metrics.
+ virtual std::shared_ptr<Metrics> GetCommitMetrics() const = 0;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/commit_context.cpp
b/src/paimon/core/operation/commit_context.cpp
new file mode 100644
index 0000000..490a075
--- /dev/null
+++ b/src/paimon/core/operation/commit_context.cpp
@@ -0,0 +1,135 @@
+/*
+ * 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/commit_context.h"
+
+#include <utility>
+
+#include "paimon/common/utils/path_util.h"
+#include "paimon/executor.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+CommitContext::CommitContext(const std::string& root_path, const std::string&
commit_user,
+ bool ignore_empty_commit, bool
use_rest_catalog_commit,
+ const std::shared_ptr<MemoryPool>& memory_pool,
+ const std::shared_ptr<Executor>& executor,
+ const std::shared_ptr<FileSystem>&
specific_file_system,
+ const std::map<std::string, std::string>& options)
+ : root_path_(root_path),
+ commit_user_(commit_user),
+ ignore_empty_commit_(ignore_empty_commit),
+ use_rest_catalog_commit_(use_rest_catalog_commit),
+ memory_pool_(memory_pool),
+ executor_(executor),
+ specific_file_system_(specific_file_system),
+ options_(options) {}
+
+CommitContext::~CommitContext() = default;
+
+class CommitContextBuilder::Impl {
+ public:
+ friend class CommitContextBuilder;
+
+ void Reset() {
+ ignore_empty_commit_ = true;
+ use_rest_catalog_commit_ = false;
+ memory_pool_ = GetDefaultPool();
+ executor_ = CreateDefaultExecutor();
+ specific_file_system_.reset();
+ options_.clear();
+ }
+
+ private:
+ std::string root_path_;
+ std::string commit_user_;
+ bool ignore_empty_commit_ = true;
+ bool use_rest_catalog_commit_ = false;
+ std::shared_ptr<MemoryPool> memory_pool_ = GetDefaultPool();
+ std::shared_ptr<Executor> executor_ = CreateDefaultExecutor();
+ std::shared_ptr<FileSystem> specific_file_system_;
+ std::map<std::string, std::string> options_;
+};
+
+CommitContextBuilder::CommitContextBuilder(const std::string& root_path,
+ const std::string& commit_user)
+ : impl_(std::make_unique<Impl>()) {
+ impl_->root_path_ = root_path;
+ impl_->commit_user_ = commit_user;
+}
+
+CommitContextBuilder::~CommitContextBuilder() = default;
+
+CommitContextBuilder& CommitContextBuilder::AddOption(const std::string& key,
+ const std::string&
value) {
+ impl_->options_[key] = value;
+ return *this;
+}
+
+CommitContextBuilder& CommitContextBuilder::SetOptions(
+ const std::map<std::string, std::string>& opts) {
+ impl_->options_ = opts;
+ return *this;
+}
+
+CommitContextBuilder& CommitContextBuilder::IgnoreEmptyCommit(bool
ignore_empty_commit) {
+ impl_->ignore_empty_commit_ = ignore_empty_commit;
+ return *this;
+}
+
+CommitContextBuilder& CommitContextBuilder::UseRESTCatalogCommit(bool
use_rest_catalog_commit) {
+ impl_->use_rest_catalog_commit_ = use_rest_catalog_commit;
+ return *this;
+}
+
+CommitContextBuilder& CommitContextBuilder::WithMemoryPool(
+ const std::shared_ptr<MemoryPool>& memory_pool) {
+ impl_->memory_pool_ = memory_pool;
+ return *this;
+}
+
+CommitContextBuilder& CommitContextBuilder::WithExecutor(
+ const std::shared_ptr<Executor>& executor) {
+ impl_->executor_ = executor;
+ return *this;
+}
+
+CommitContextBuilder& CommitContextBuilder::WithFileSystem(
+ const std::shared_ptr<FileSystem>& file_system) {
+ impl_->specific_file_system_ = file_system;
+ return *this;
+}
+
+Result<std::unique_ptr<CommitContext>> CommitContextBuilder::Finish() {
+ PAIMON_ASSIGN_OR_RAISE(impl_->root_path_,
PathUtil::NormalizePath(impl_->root_path_));
+ if (impl_->root_path_.empty()) {
+ return Status::Invalid("root path is empty");
+ }
+ auto ctx = std::make_unique<CommitContext>(
+ impl_->root_path_, impl_->commit_user_, impl_->ignore_empty_commit_,
+ impl_->use_rest_catalog_commit_, impl_->memory_pool_, impl_->executor_,
+ impl_->specific_file_system_, impl_->options_);
+ impl_->Reset();
+ return ctx;
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/file_store_commit.cpp
b/src/paimon/core/operation/file_store_commit.cpp
new file mode 100644
index 0000000..8cc4ba9
--- /dev/null
+++ b/src/paimon/core/operation/file_store_commit.cpp
@@ -0,0 +1,140 @@
+/*
+ * 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/file_store_commit.h"
+
+#include <cassert>
+#include <utility>
+
+#include "paimon/commit_context.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/binary_row_partition_computer.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/manifest/index_manifest_entry.h"
+#include "paimon/core/manifest/index_manifest_file.h"
+#include "paimon/core/manifest/manifest_file.h"
+#include "paimon/core/manifest/manifest_list.h"
+#include "paimon/core/operation/expire_snapshots.h"
+#include "paimon/core/operation/file_store_commit_impl.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.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/format/file_format.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/result.h"
+
+namespace arrow {
+class Schema;
+} // namespace arrow
+
+namespace paimon {
+
+Result<std::unique_ptr<FileStoreCommit>> FileStoreCommit::Create(
+ std::unique_ptr<CommitContext> ctx) {
+ if (ctx == nullptr) {
+ return Status::Invalid("commit context is null pointer");
+ }
+ if (ctx->GetMemoryPool() == nullptr) {
+ return Status::Invalid("memory pool is null pointer");
+ }
+ if (ctx->GetExecutor() == nullptr) {
+ return Status::Invalid("executor is null pointer");
+ }
+
+ PAIMON_ASSIGN_OR_RAISE(CoreOptions tmp_options,
+ CoreOptions::FromMap(ctx->GetOptions(),
ctx->GetSpecificFileSystem()));
+ const std::string& root_path = ctx->GetRootPath();
+ auto schema_manager =
std::make_shared<SchemaManager>(tmp_options.GetFileSystem(), root_path);
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>>
table_schema,
+ schema_manager->Latest());
+ if (table_schema == std::nullopt) {
+ return Status::Invalid("not found latest schema");
+ }
+ const auto& schema = table_schema.value();
+ if (!schema->PrimaryKeys().empty() &&
+ ctx->GetOptions().find("enable-pk-commit-in-inte-test") ==
ctx->GetOptions().end()) {
+ return Status::NotImplemented("not support pk table commit yet");
+ }
+ auto opts = schema->Options();
+ for (const auto& [key, value] : ctx->GetOptions()) {
+ opts[key] = value;
+ }
+ std::shared_ptr<arrow::Schema> arrow_schema =
+ DataField::ConvertDataFieldsToArrowSchema(schema->Fields());
+ PAIMON_ASSIGN_OR_RAISE(CoreOptions options,
+ CoreOptions::FromMap(opts,
ctx->GetSpecificFileSystem()));
+ assert(options.GetFileSystem());
+ assert(options.GetFileFormat());
+ PAIMON_ASSIGN_OR_RAISE(bool is_object_store,
FileSystem::IsObjectStore(root_path));
+ if (is_object_store &&
opts.find("enable-object-store-commit-in-inte-test") == opts.end()) {
+ return Status::NotImplemented(
+ "commit operation does not support object store file system for
now");
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<BinaryRowPartitionComputer> partition_computer,
+ BinaryRowPartitionComputer::Create(
+ table_schema.value()->PartitionKeys(), arrow_schema,
options.GetPartitionDefaultName(),
+ options.LegacyPartitionNameEnabled(), ctx->GetMemoryPool()));
+ PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> external_paths,
options.CreateExternalPaths());
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::string>
global_index_external_path,
+ options.CreateGlobalIndexExternalPath());
+
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<FileStorePathFactory> path_factory,
+ FileStorePathFactory::Create(
+ root_path, arrow_schema, table_schema.value()->PartitionKeys(),
+ options.GetPartitionDefaultName(),
options.GetFileFormat()->Identifier(),
+ options.DataFilePrefix(), options.LegacyPartitionNameEnabled(),
external_paths,
+ global_index_external_path, options.IndexFileInDataFileDir(),
ctx->GetMemoryPool()));
+
+ auto snapshot_manager =
std::make_shared<SnapshotManager>(options.GetFileSystem(), root_path);
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<ManifestList> manifest_list,
+ ManifestList::Create(options.GetFileSystem(),
options.GetManifestFormat(),
+ options.GetManifestCompression(), path_factory,
ctx->GetMemoryPool()));
+
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<arrow::Schema> partition_schema,
+ FieldMapping::GetPartitionSchema(arrow_schema,
table_schema.value()->PartitionKeys()));
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<ManifestFile> manifest_file,
+ ManifestFile::Create(options.GetFileSystem(),
options.GetManifestFormat(),
+ options.GetManifestCompression(), path_factory,
+ options.GetManifestTargetFileSize(),
ctx->GetMemoryPool(), options,
+ partition_schema));
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<IndexManifestFile> index_manifest_file,
+ IndexManifestFile::Create(options.GetFileSystem(),
options.GetManifestFormat(),
+ options.GetManifestCompression(),
path_factory,
+ options.GetBucket(), ctx->GetMemoryPool(),
options));
+
+ auto expire_snapshots = std::make_shared<ExpireSnapshots>(
+ snapshot_manager, path_factory, manifest_list, manifest_file,
options.GetFileSystem(),
+ options.GetExpireConfig(), ctx->GetExecutor());
+
+ return std::make_unique<FileStoreCommitImpl>(
+ ctx->GetMemoryPool(), ctx->GetExecutor(), arrow_schema, root_path,
ctx->GetCommitUser(),
+ options, path_factory, std::move(partition_computer), snapshot_manager,
+ ctx->IgnoreEmptyCommit(), ctx->UseRESTCatalogCommit(),
table_schema.value(), manifest_file,
+ manifest_list, index_manifest_file, expire_snapshots, schema_manager);
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/file_store_commit_impl.cpp
b/src/paimon/core/operation/file_store_commit_impl.cpp
new file mode 100644
index 0000000..06eeeae
--- /dev/null
+++ b/src/paimon/core/operation/file_store_commit_impl.cpp
@@ -0,0 +1,1002 @@
+/*
+ * 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/file_store_commit_impl.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <future>
+#include <list>
+#include <set>
+#include <unordered_map>
+#include <utility>
+
+#include "fmt/format.h"
+#include "fmt/ranges.h"
+#include "paimon/commit_message.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/data/blob_utils.h"
+#include "paimon/common/executor/future.h"
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/common/table/special_fields.h"
+#include "paimon/common/utils/binary_row_partition_computer.h"
+#include "paimon/common/utils/date_time_utils.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/catalog/catalog_snapshot_commit.h"
+#include "paimon/core/catalog/renaming_snapshot_commit.h"
+#include "paimon/core/catalog/snapshot_commit.h"
+#include "paimon/core/deletionvectors/deletion_vectors_index_file.h"
+#include "paimon/core/index/index_file_meta.h"
+#include "paimon/core/io/compact_increment.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/data_increment.h"
+#include "paimon/core/manifest/file_entry.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/core/manifest/file_source.h"
+#include "paimon/core/manifest/index_manifest_file.h"
+#include "paimon/core/manifest/manifest_committable.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/manifest/partition_entry.h"
+#include "paimon/core/operation/append_only_file_store_scan.h"
+#include "paimon/core/operation/expire_snapshots.h"
+#include "paimon/core/operation/file_store_scan.h"
+#include "paimon/core/operation/manifest_file_merger.h"
+#include "paimon/core/operation/metrics/commit_metrics.h"
+#include "paimon/core/partition/partition_statistics.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.h"
+#include "paimon/core/table/sink/commit_message_impl.h"
+#include "paimon/core/utils/duration.h"
+#include "paimon/core/utils/file_store_path_factory.h"
+#include "paimon/core/utils/snapshot_manager.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/logging.h"
+#include "paimon/metrics.h"
+#include "paimon/scan_context.h"
+
+namespace paimon {
+class Executor;
+class MemoryPool;
+
+FileStoreCommitImpl::FileStoreCommitImpl(
+ const std::shared_ptr<MemoryPool>& pool, const std::shared_ptr<Executor>&
executor,
+ const std::shared_ptr<arrow::Schema>& schema, const std::string& root_path,
+ const std::string& commit_user, const CoreOptions& options,
+ const std::shared_ptr<FileStorePathFactory>& path_factory,
+ std::unique_ptr<BinaryRowPartitionComputer> partition_computer,
+ const std::shared_ptr<SnapshotManager>& snapshot_manager, bool
ignore_empty_commit,
+ bool use_rest_catalog_commit, const std::shared_ptr<TableSchema>&
table_schema,
+ const std::shared_ptr<ManifestFile>& manifest_file,
+ const std::shared_ptr<ManifestList>& manifest_list,
+ const std::shared_ptr<IndexManifestFile>& index_manifest_file,
+ const std::shared_ptr<ExpireSnapshots>& expire_snapshots,
+ const std::shared_ptr<SchemaManager>& schema_manager)
+ : memory_pool_(pool),
+ executor_(executor),
+ schema_(schema),
+ root_path_(root_path),
+ commit_user_(commit_user),
+ options_(options),
+ path_factory_(path_factory),
+ fs_(options.GetFileSystem()),
+ partition_computer_(std::move(partition_computer)),
+ snapshot_manager_(snapshot_manager),
+ ignore_empty_commit_(ignore_empty_commit),
+ num_bucket_(options.GetBucket()),
+ table_schema_(table_schema),
+ manifest_file_(manifest_file),
+ manifest_list_(manifest_list),
+ index_manifest_file_(index_manifest_file),
+ expire_snapshots_(expire_snapshots),
+ schema_manager_(schema_manager),
+ metrics_(std::make_shared<MetricsImpl>()),
+ logger_(Logger::GetLogger("FileStoreCommitImpl")) {
+ if (use_rest_catalog_commit) {
+ snapshot_commit_ = std::make_shared<CatalogSnapshotCommit>();
+ } else {
+ snapshot_commit_ = std::make_shared<RenamingSnapshotCommit>(fs_,
snapshot_manager_);
+ }
+}
+
+FileStoreCommitImpl::~FileStoreCommitImpl() = default;
+
+Result<int32_t> FileStoreCommitImpl::Expire() {
+ return expire_snapshots_->Expire();
+}
+
+Status FileStoreCommitImpl::DropPartition(
+ const std::vector<std::map<std::string, std::string>>& partitions, int64_t
commit_identifier) {
+ if (partitions.empty()) {
+ return Status::Invalid("Drop partition failed: partitions list cannot
be empty.");
+ }
+ std::string log_msg = fmt::format("Ready to drop partitions {}",
partitions);
+ PAIMON_LOG_DEBUG(logger_, "%s", log_msg.c_str());
+ return TryOverwrite(partitions, {}, commit_identifier, std::nullopt);
+}
+
+Result<int32_t> FileStoreCommitImpl::FilterAndCommit(
+ const std::map<int64_t, std::vector<std::shared_ptr<CommitMessage>>>&
+ commit_identifier_and_messages,
+ std::optional<int64_t> watermark) {
+ std::vector<std::shared_ptr<ManifestCommittable>> committables;
+ for (const auto& [identifier, msgs] : commit_identifier_and_messages) {
+ committables.push_back(CreateManifestCommittable(identifier, msgs,
watermark));
+ }
+
+ PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<ManifestCommittable>>
retry_committables,
+ FilterCommitted(committables));
+ if (!retry_committables.empty()) {
+ PAIMON_RETURN_NOT_OK(CheckFilesExistence(retry_committables));
+ for (const auto& committable : retry_committables) {
+ PAIMON_RETURN_NOT_OK(Commit(committable,
/*check_append_files=*/true));
+ }
+ }
+ return retry_committables.size();
+}
+
+Status FileStoreCommitImpl::CheckFilesExistence(
+ const std::vector<std::shared_ptr<ManifestCommittable>>& committables)
const {
+ std::vector<std::string> all_paths;
+ for (const auto& committable : committables) {
+ for (const auto& message : committable->FileCommittables()) {
+ auto msg = dynamic_cast<CommitMessageImpl*>(message.get());
+ if (msg) {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<DataFilePathFactory>
data_file_path_factory,
+ path_factory_->CreateDataFilePathFactory(msg->Partition(),
msg->Bucket()));
+ auto collect_files =
+ [&all_paths, data_file_path_factory](
+ const std::vector<std::shared_ptr<DataFileMeta>>&
file_metas) {
+ for (const auto& file_meta : file_metas) {
+ auto paths =
data_file_path_factory->CollectFiles(file_meta);
+ all_paths.insert(all_paths.end(), paths.begin(),
paths.end());
+ }
+ };
+ // skip compact before files, deleted index files
+ DataIncrement new_files_increment =
msg->GetNewFilesIncrement();
+ collect_files(new_files_increment.NewFiles());
+ collect_files(new_files_increment.ChangelogFiles());
+ auto new_data_index_metas =
new_files_increment.NewIndexFiles();
+ for (const auto& data_index_meta : new_data_index_metas) {
+ all_paths.push_back(
+
path_factory_->ToIndexFilePath(data_index_meta->FileName()));
+ }
+
+ CompactIncrement compact_increment =
msg->GetCompactIncrement();
+ collect_files(compact_increment.CompactBefore());
+ collect_files(compact_increment.CompactAfter());
+ auto new_compact_index_metas =
compact_increment.NewIndexFiles();
+ for (const auto& compact_index_meta : new_compact_index_metas)
{
+ all_paths.push_back(
+
path_factory_->ToIndexFilePath(compact_index_meta->FileName()));
+ }
+ } else {
+ return Status::Invalid("fail to cast commit message to impl");
+ }
+ }
+ }
+ std::vector<std::future<Result<std::pair<bool, std::string>>>>
file_exists_futures;
+ for (const auto& path : all_paths) {
+ file_exists_futures.push_back(
+ Via(executor_.get(), [this, path]() -> Result<std::pair<bool,
std::string>> {
+ PAIMON_ASSIGN_OR_RAISE(bool exist, fs_->Exists(path));
+ return std::pair(exist, path);
+ }));
+ }
+ int32_t not_exist_files_count = 0;
+ std::vector<Result<std::pair<bool, std::string>>> file_exists =
CollectAll(file_exists_futures);
+ std::vector<std::string> non_exist_files;
+ for (auto file_exist : file_exists) {
+ if (!file_exist.ok()) {
+ return file_exist.status();
+ }
+ if (!file_exist.value().first) {
+ not_exist_files_count++;
+ non_exist_files.push_back(file_exist.value().second);
+ }
+ }
+
+ if (not_exist_files_count > 0) {
+ return Status::Invalid(fmt::format(
+ "Cannot recover from this checkpoint because some files in the
snapshot that need to "
+ "be resubmitted have been deleted: {}. The most likely reason is
because you are "
+ "recovering from a very old savepoint that contains some
uncommitted files that have "
+ "already been deleted.",
+ fmt::join(non_exist_files, ", ")));
+ }
+ return Status::OK();
+}
+
+Result<std::vector<std::shared_ptr<ManifestCommittable>>>
FileStoreCommitImpl::FilterCommitted(
+ const std::vector<std::shared_ptr<ManifestCommittable>>& committables) {
+ // nothing to filter, fast exit
+ if (committables.empty()) {
+ return committables;
+ }
+
+ for (size_t i = 1; i < committables.size(); i++) {
+ if (committables[i]->Identifier() < committables[i - 1]->Identifier())
{
+ return Status::Invalid(
+ "Committables must be sorted according to identifiers before
filtering. This is "
+ "unexpected.");
+ }
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
+
snapshot_manager_->LatestSnapshotOfUser(commit_user_));
+ if (latest_snapshot) {
+ std::vector<std::shared_ptr<ManifestCommittable>> result;
+ for (const auto& committable : committables) {
+ // if committable is newer than latest snapshot, then it hasn't
been committed
+ if (committable->Identifier() >
latest_snapshot.value().CommitIdentifier()) {
+ result.push_back(committable);
+ } else {
+ // TODO(yonghao.fyh): callback
+ }
+ }
+ return result;
+ } else {
+ // if there is no previous snapshots then nothing should be filtered
+ return committables;
+ }
+}
+
+Status FileStoreCommitImpl::Overwrite(
+ const std::vector<std::map<std::string, std::string>>& partitions,
+ const std::vector<std::shared_ptr<CommitMessage>>& commit_messages,
int64_t identifier,
+ std::optional<int64_t> watermark) {
+ std::shared_ptr<ManifestCommittable> committable =
+ CreateManifestCommittable(identifier, commit_messages, watermark);
+ std::vector<ManifestEntry> append_table_files;
+ std::vector<ManifestEntry> append_changelog_files;
+ std::vector<ManifestEntry> compact_table_files;
+ std::vector<ManifestEntry> compact_changelog_files;
+ std::vector<IndexManifestEntry> append_table_index_files;
+ std::vector<IndexManifestEntry> compact_table_index_files;
+ PAIMON_RETURN_NOT_OK(CollectChanges(committable->FileCommittables(),
&append_table_files,
+ &append_changelog_files,
&compact_table_files,
+ &compact_changelog_files,
&append_table_index_files,
+ &compact_table_index_files));
+ if (!append_table_index_files.empty()) {
+ return Status::NotImplemented("Overwrite not support index for now");
+ }
+ return TryOverwrite(partitions, append_table_files, identifier, watermark);
+}
+
+Result<int32_t> FileStoreCommitImpl::FilterAndOverwrite(
+ const std::vector<std::map<std::string, std::string>>& partitions,
+ const std::vector<std::shared_ptr<CommitMessage>>& commit_messages,
int64_t identifier,
+ std::optional<int64_t> watermark) {
+ std::shared_ptr<ManifestCommittable> committable =
+ CreateManifestCommittable(identifier, commit_messages, watermark);
+ std::vector<std::shared_ptr<ManifestCommittable>> committables;
+ committables.push_back(committable);
+ PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<ManifestCommittable>>
actual_committables,
+ FilterCommitted(committables));
+ if (!actual_committables.empty()) {
+ std::vector<ManifestEntry> append_table_files;
+ std::vector<ManifestEntry> append_changelog_files;
+ std::vector<ManifestEntry> compact_table_files;
+ std::vector<ManifestEntry> compact_changelog_files;
+ std::vector<IndexManifestEntry> append_table_index_files;
+ std::vector<IndexManifestEntry> compact_table_index_files;
+
PAIMON_RETURN_NOT_OK(CollectChanges(actual_committables[0]->FileCommittables(),
+ &append_table_files,
&append_changelog_files,
+ &compact_table_files,
&compact_changelog_files,
+ &append_table_index_files,
&compact_table_index_files));
+ if (!append_table_index_files.empty()) {
+ return Status::NotImplemented("FilterAndOverwrite not support
index for now");
+ }
+ PAIMON_RETURN_NOT_OK(TryOverwrite(partitions, append_table_files,
identifier, watermark));
+ }
+ return actual_committables.size();
+}
+
+Result<std::string> FileStoreCommitImpl::GetLastCommitTableRequest() {
+ return snapshot_commit_->GetLastCommitTableRequest();
+}
+
+Result<std::vector<ManifestEntry>> FileStoreCommitImpl::GetAllFiles(
+ const Snapshot& snapshot, const std::vector<std::map<std::string,
std::string>>& partitions) {
+ auto scan_filter = std::make_shared<ScanFilter>(/*predicate=*/nullptr,
partitions,
+
/*bucket_filter=*/std::nullopt);
+ PAIMON_ASSIGN_OR_RAISE(
+ auto scan, AppendOnlyFileStoreScan::Create(
+ snapshot_manager_, schema_manager_, manifest_list_,
manifest_file_,
+ table_schema_, schema_, scan_filter, options_,
executor_, memory_pool_));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileStoreScan::RawPlan> plan,
+ scan->WithSnapshot(snapshot)->CreatePlan());
+ // scan existing file metas
+ return plan->Files();
+}
+
+Status FileStoreCommitImpl::TryOverwrite(
+ const std::vector<std::map<std::string, std::string>>& partitions,
+ const std::vector<ManifestEntry>& changes, int64_t commit_identifier,
+ std::optional<int64_t> watermark) {
+ int32_t retry_count = 0;
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
+ snapshot_manager_->LatestSnapshot());
+ std::vector<ManifestEntry> changes_with_overwrite;
+ if (latest_snapshot) {
+ PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestEntry> entries,
+ GetAllFiles(latest_snapshot.value(),
partitions));
+ for (const auto& entry : entries) {
+ changes_with_overwrite.emplace_back(FileKind::Delete(),
entry.Partition(),
+ entry.Bucket(),
entry.TotalBuckets(),
+ entry.File());
+ }
+ }
+ changes_with_overwrite.insert(changes_with_overwrite.end(),
changes.begin(), changes.end());
+ PAIMON_ASSIGN_OR_RAISE(bool commit_success,
+ TryCommitOnce(changes_with_overwrite,
/*index_entries=*/{},
+ commit_identifier, watermark,
+ /*log_offsets=*/{},
/*properties=*/{},
+
Snapshot::CommitKind::Overwrite(), latest_snapshot,
+ /*need_conflict_check=*/true));
+ if (commit_success) {
+ break;
+ }
+ if (retry_count >= options_.GetCommitMaxRetries()) {
+ return Status::Invalid(
+ fmt::format("Commit failed after {} attempts, there maybe
exist commit conflicts "
+ "between multiple jobs.",
+ options_.GetCommitMaxRetries()));
+ }
+ retry_count++;
+ }
+ return Status::OK();
+}
+
+Status FileStoreCommitImpl::Commit(const std::shared_ptr<ManifestCommittable>&
committable,
+ bool check_append_files) {
+ std::vector<ManifestEntry> append_table_files;
+ std::vector<ManifestEntry> append_changelog_files;
+ std::vector<ManifestEntry> compact_table_files;
+ std::vector<ManifestEntry> compact_changelog_files;
+ std::vector<IndexManifestEntry> append_table_index_files;
+ std::vector<IndexManifestEntry> compact_table_index_files;
+ PAIMON_RETURN_NOT_OK(CollectChanges(committable->FileCommittables(),
&append_table_files,
+ &append_changelog_files,
&compact_table_files,
+ &compact_changelog_files,
&append_table_index_files,
+ &compact_table_index_files));
+
+ int32_t attempt = 0;
+ int32_t generated_snapshot = 0;
+ Duration duration;
+ if (!ignore_empty_commit_ || !append_table_files.empty() ||
!append_table_index_files.empty()) {
+ PAIMON_ASSIGN_OR_RAISE(int32_t cnt,
+ TryCommit(append_table_files,
append_table_index_files,
+ committable->Identifier(),
committable->Watermark(),
+ committable->LogOffsets(),
committable->Properties(),
+ Snapshot::CommitKind::Append(),
check_append_files));
+ attempt += cnt;
+ ++generated_snapshot;
+ }
+
+ if (!compact_table_files.empty() || !compact_table_index_files.empty()) {
+ PAIMON_ASSIGN_OR_RAISE(
+ int32_t cnt, TryCommit(compact_table_files,
compact_table_index_files,
+ committable->Identifier(),
committable->Watermark(),
+ committable->LogOffsets(),
committable->Properties(),
+ Snapshot::CommitKind::Compact(),
/*check_append_files=*/true));
+ attempt += cnt;
+ ++generated_snapshot;
+ }
+ auto table_files_added = static_cast<int32_t>(append_table_files.size());
+ int32_t table_files_deleted = 0;
+ int64_t compaction_input_file_size = 0;
+ int64_t compaction_output_file_size = 0;
+ for (const auto& entry : compact_table_files) {
+ const auto& kind = entry.Kind();
+ if (kind == FileKind::Add()) {
+ ++table_files_added;
+ compaction_output_file_size += entry.File()->file_size;
+ } else if (kind == FileKind::Delete()) {
+ ++table_files_deleted;
+ compaction_input_file_size += entry.File()->file_size;
+ }
+ }
+ metrics_->SetCounter(CommitMetrics::LAST_COMMIT_DURATION, duration.Get());
+ metrics_->SetCounter(CommitMetrics::LAST_COMMIT_ATTEMPTS, attempt);
+ metrics_->SetCounter(CommitMetrics::LAST_TABLE_FILES_ADDED,
table_files_added);
+ metrics_->SetCounter(CommitMetrics::LAST_TABLE_FILES_DELETED,
table_files_deleted);
+ metrics_->SetCounter(CommitMetrics::LAST_TABLE_FILES_APPENDED,
append_table_files.size());
+ metrics_->SetCounter(CommitMetrics::LAST_TABLE_FILES_COMMIT_COMPACTED,
+ compact_table_files.size());
+ metrics_->SetCounter(CommitMetrics::LAST_CHANGELOG_FILES_APPENDED,
+ append_changelog_files.size());
+ metrics_->SetCounter(CommitMetrics::LAST_CHANGELOG_FILES_COMMIT_COMPACTED,
+ compact_changelog_files.size());
+ metrics_->SetCounter(CommitMetrics::LAST_GENERATED_SNAPSHOTS,
generated_snapshot);
+ metrics_->SetCounter(CommitMetrics::LAST_DELTA_RECORDS_APPENDED,
RowCounts(append_table_files));
+ metrics_->SetCounter(CommitMetrics::LAST_CHANGELOG_RECORDS_APPENDED,
+ RowCounts(append_changelog_files));
+ metrics_->SetCounter(CommitMetrics::LAST_DELTA_RECORDS_COMMIT_COMPACTED,
+ RowCounts(compact_table_files));
+
metrics_->SetCounter(CommitMetrics::LAST_CHANGELOG_RECORDS_COMMIT_COMPACTED,
+ RowCounts(compact_changelog_files));
+ metrics_->SetCounter(CommitMetrics::LAST_PARTITIONS_WRITTEN,
+ NumChangedPartitions({append_table_files,
compact_table_files}));
+ metrics_->SetCounter(CommitMetrics::LAST_BUCKETS_WRITTEN,
+ NumChangedBuckets({append_table_files,
compact_table_files}));
+ metrics_->SetCounter(CommitMetrics::LAST_COMPACTION_INPUT_FILE_SIZE,
+ compaction_input_file_size);
+ metrics_->SetCounter(CommitMetrics::LAST_COMPACTION_OUTPUT_FILE_SIZE,
+ compaction_output_file_size);
+ return Status::OK();
+}
+
+Status FileStoreCommitImpl::Commit(
+ const std::vector<std::shared_ptr<CommitMessage>>& commit_messages,
int64_t identifier,
+ std::optional<int64_t> watermark) {
+ std::shared_ptr<ManifestCommittable> committable =
+ CreateManifestCommittable(identifier, commit_messages, watermark);
+ return Commit(committable, /*check_append_files=*/false);
+}
+
+Result<int32_t> FileStoreCommitImpl::TryCommit(const
std::vector<ManifestEntry>& delta_files,
+ const
std::vector<IndexManifestEntry>& index_entries,
+ int64_t identifier,
std::optional<int64_t> watermark,
+ std::map<int32_t, int64_t>
log_offsets,
+ const std::map<std::string,
std::string>& properties,
+ Snapshot::CommitKind
commit_kind,
+ bool check_append_files) {
+ int32_t retry_count = 0;
+ int64_t start_millis = DateTimeUtils::GetCurrentUTCTimeUs() / 1000;
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
+ snapshot_manager_->LatestSnapshot());
+ PAIMON_ASSIGN_OR_RAISE(
+ bool commit_success,
+ TryCommitOnce(delta_files, index_entries, identifier, watermark,
log_offsets,
+ properties, commit_kind, latest_snapshot,
check_append_files));
+ if (commit_success) {
+ break;
+ }
+ int64_t current_millis = DateTimeUtils::GetCurrentUTCTimeUs() / 1000;
+ if (current_millis - start_millis > options_.GetCommitTimeout() ||
+ retry_count >= options_.GetCommitMaxRetries()) {
+ return Status::Invalid(
+ fmt::format("Commit failed after {} millis with {} retries,
there maybe exist "
+ "commit conflicts between multiple jobs.",
+ options_.GetCommitTimeout(),
options_.GetCommitMaxRetries()));
+ }
+ retry_count++;
+ }
+ return retry_count + 1;
+}
+
+Result<std::set<std::map<std::string, std::string>>>
FileStoreCommitImpl::ChangedPartitions(
+ const std::vector<ManifestEntry>& data_files,
+ const std::vector<IndexManifestEntry>& index_files) const {
+ std::set<std::map<std::string, std::string>> partitions;
+ auto add_partition = [&, this](const BinaryRow& partition_row) -> Status {
+ std::vector<std::pair<std::string, std::string>> part_values;
+ PAIMON_ASSIGN_OR_RAISE(part_values,
+
partition_computer_->GeneratePartitionVector(partition_row));
+ if (part_values.empty()) {
+ return Status::OK();
+ }
+ std::map<std::string, std::string> part_values_map;
+ for (const auto& [key, value] : part_values) {
+ part_values_map[key] = value;
+ }
+ partitions.insert(part_values_map);
+ return Status::OK();
+ };
+
+ for (const ManifestEntry& entry : data_files) {
+ PAIMON_RETURN_NOT_OK(add_partition(entry.Partition()));
+ }
+ for (const IndexManifestEntry& entry : index_files) {
+ if (entry.index_file->IndexType() ==
DeletionVectorsIndexFile::DELETION_VECTORS_INDEX) {
+ PAIMON_RETURN_NOT_OK(add_partition(entry.partition));
+ }
+ }
+ return partitions;
+}
+
+Result<std::vector<ManifestEntry>>
FileStoreCommitImpl::ReadAllEntriesFromChangedPartitions(
+ const Snapshot& latest_snapshot,
+ const std::set<std::map<std::string, std::string>>& partitions) const {
+ std::vector<std::map<std::string, std::string>>
partition_filters(partitions.begin(),
+
partitions.end());
+ auto scan_filter = std::make_shared<ScanFilter>(/*predicate=*/nullptr,
partition_filters,
+
/*bucket_filter=*/std::nullopt);
+ PAIMON_ASSIGN_OR_RAISE(
+ auto scan, AppendOnlyFileStoreScan::Create(
+ snapshot_manager_, schema_manager_, manifest_list_,
manifest_file_,
+ table_schema_, schema_, scan_filter, options_,
executor_, memory_pool_));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FileStoreScan::RawPlan> plan,
+ scan->WithSnapshot(latest_snapshot)->CreatePlan());
+ // scan existing file metas
+ return plan->Files();
+}
+
+Status FileStoreCommitImpl::NoConflictsOrFail(const std::string&
base_commit_user,
+ const
std::vector<ManifestEntry>& base_entries,
+ const
std::vector<ManifestEntry>& changes) const {
+ ScopeGuard guard([&]() {
+ PAIMON_LOG_WARN(logger_, "File deletion conflicts detected! Give up
committing. %s",
+ base_commit_user.c_str());
+ });
+ std::vector<ManifestEntry> all_entries = base_entries;
+ all_entries.insert(all_entries.end(), changes.begin(), changes.end());
+ std::vector<ManifestEntry> merged_entries;
+ PAIMON_RETURN_NOT_OK(FileEntry::MergeEntries(all_entries,
&merged_entries));
+ for (const auto& entry : merged_entries) {
+ if (entry.Kind() == FileKind::Delete()) {
+ return Status::Invalid(fmt::format(
+ "Trying to delete file {} which is not previously added.",
entry.FileName()));
+ }
+ }
+ // TODO(yonghao.fyh): check for all LSM level >= 1, key ranges of files do
not intersect
+ guard.Release();
+ return Status::OK();
+}
+
+Result<bool> FileStoreCommitImpl::TryCommitOnce(
+ const std::vector<ManifestEntry>& delta_entries,
+ const std::vector<IndexManifestEntry>& index_entries, int64_t identifier,
+ std::optional<int64_t> watermark, std::map<int32_t, int64_t> log_offsets,
+ const std::map<std::string, std::string>& properties, Snapshot::CommitKind
commit_kind,
+ const std::optional<Snapshot>& latest_snapshot, bool need_conflict_check) {
+ std::vector<ManifestEntry> delta_files = delta_entries;
+ int64_t start_millis = DateTimeUtils::GetCurrentUTCTimeUs() / 1000;
+ int64_t new_snapshot_id = Snapshot::FIRST_SNAPSHOT_ID;
+ int64_t first_row_id_start = 0;
+ if (latest_snapshot) {
+ new_snapshot_id = latest_snapshot.value().Id() + 1;
+ std::optional<int64_t> next_row_id =
latest_snapshot.value().NextRowId();
+ if (next_row_id) {
+ first_row_id_start = next_row_id.value();
+ }
+ }
+
+ PAIMON_LOG_DEBUG(logger_, "Ready to commit table files to snapshot #%ld",
new_snapshot_id);
+ for (const ManifestEntry& entry : delta_files) {
+ PAIMON_LOG_DEBUG(logger_, " * %s", entry.ToString().c_str());
+ }
+
+ if (need_conflict_check && latest_snapshot) {
+ std::set<std::map<std::string, std::string>> changed_partitions;
+ PAIMON_ASSIGN_OR_RAISE(changed_partitions,
ChangedPartitions(delta_files, index_entries));
+ PAIMON_ASSIGN_OR_RAISE(
+ std::vector<ManifestEntry> base_data_files,
+ ReadAllEntriesFromChangedPartitions(latest_snapshot.value(),
changed_partitions));
+ PAIMON_RETURN_NOT_OK(
+ NoConflictsOrFail(latest_snapshot.value().CommitUser(),
base_data_files, delta_files));
+ }
+
+ std::vector<ManifestFileMeta> merge_before_manifests;
+ std::vector<ManifestFileMeta> merge_after_manifests;
+ std::pair<std::string, int64_t> base_manifest_list;
+ std::pair<std::string, int64_t> delta_manifest_list;
+ std::vector<PartitionEntry> delta_statistics;
+ std::string new_snapshot_path;
+
+ std::optional<std::string> old_index_manifest;
+ std::optional<std::string> index_manifest_name;
+ ScopeGuard guard([&]() {
+ int64_t commit_time = ((DateTimeUtils::GetCurrentUTCTimeUs() / 1000) -
start_millis) / 1000;
+ PAIMON_LOG_WARN(logger_,
+ "Atomic commit failed for snapshot #%ld (path %s) by
user %s with "
+ "identifier %ld and kind %s after %ld seconds. Clean
up and try again.",
+ new_snapshot_id, new_snapshot_path.c_str(),
commit_user_.c_str(),
+ identifier,
Snapshot::CommitKind::ToString(commit_kind).c_str(),
+ commit_time);
+
+ CleanUpTmpManifests(base_manifest_list.first,
delta_manifest_list.first,
+ merge_before_manifests, merge_after_manifests,
old_index_manifest,
+ index_manifest_name);
+ });
+ int64_t next_row_id_start = first_row_id_start;
+ int64_t previous_total_record_count = 0;
+
+ if (latest_snapshot) {
+ old_index_manifest = latest_snapshot.value().IndexManifest();
+ // TODO(yonghao.fyh): total record count should call scan when its
std::nullopt
+ previous_total_record_count =
latest_snapshot.value().TotalRecordCount() != std::nullopt
+ ?
latest_snapshot.value().TotalRecordCount().value()
+ : 0;
+ std::vector<ManifestFileMeta> previous_manifests;
+ // read all previous manifest files
+ PAIMON_RETURN_NOT_OK(
+ manifest_list_->ReadDataManifests(latest_snapshot.value(),
&previous_manifests));
+ merge_before_manifests.insert(merge_before_manifests.end(),
previous_manifests.begin(),
+ previous_manifests.end());
+ // read the last snapshot to complete the bucket's offsets when
logOffsets does not
+ // contain all buckets
+ std::optional<std::map<int32_t, int64_t>> latest_log_offsets =
+ latest_snapshot.value().LogOffsets();
+ if (latest_log_offsets) {
+ for (const auto& [key, value] : latest_log_offsets.value()) {
+ log_offsets.emplace(key, value);
+ }
+ }
+ std::optional<int64_t> latest_watermark =
latest_snapshot.value().Watermark();
+ if (latest_watermark) {
+ if (watermark == std::nullopt) {
+ watermark = latest_watermark;
+ } else {
+ watermark = std::max(watermark.value(),
latest_watermark.value());
+ }
+ }
+ }
+
+ // try to merge old manifest files to create base manifest list
+ PAIMON_ASSIGN_OR_RAISE(
+ std::vector<ManifestFileMeta> merged_metas,
+ ManifestFileMerger::Merge(merge_before_manifests,
options_.GetManifestTargetFileSize(),
+ options_.GetManifestMergeMinCount(),
+
options_.GetManifestFullCompactionThresholdSize(),
+ manifest_file_.get()));
+ merge_after_manifests.insert(merge_after_manifests.end(),
merged_metas.begin(),
+ merged_metas.end());
+ PAIMON_ASSIGN_OR_RAISE(base_manifest_list,
manifest_list_->Write(merge_after_manifests));
+
+ if (options_.RowTrackingEnabled()) {
+ if (options_.RowTrackingPartitionGroupOnCommit()) {
+ std::unordered_map<BinaryRow, std::vector<ManifestEntry>>
delta_files_by_partition;
+ for (auto& entry : delta_files) {
+
delta_files_by_partition[entry.Partition()].push_back(std::move(entry));
+ }
+ delta_files.clear();
+ for (auto& [_, entries] : delta_files_by_partition) {
+ delta_files.insert(delta_files.end(),
std::make_move_iterator(entries.begin()),
+ std::make_move_iterator(entries.end()));
+ }
+ }
+ // assigned snapshot id to delta files
+ AssignSnapshotId(new_snapshot_id, &delta_files);
+ // assign row id for new files
+ PAIMON_ASSIGN_OR_RAISE(next_row_id_start,
+ AssignRowTrackingMeta(first_row_id_start,
&delta_files));
+ }
+
+ // the added records subtract the deleted records from
+ int64_t delta_record_count =
+ ManifestEntry::RecordCountAdd(delta_files) -
ManifestEntry::RecordCountDelete(delta_files);
+ int64_t total_record_count = previous_total_record_count +
delta_record_count;
+
+ // write new delta files into manifest files
+ std::unordered_map<BinaryRow, PartitionEntry> partition_entry_map;
+ PAIMON_RETURN_NOT_OK(PartitionEntry::Merge(delta_files,
&partition_entry_map));
+ delta_statistics.reserve(partition_entry_map.size());
+ for (const auto& [_, partition_entry] : partition_entry_map) {
+ delta_statistics.push_back(partition_entry);
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestFileMeta> new_changes_manifests,
+ manifest_file_->Write(delta_files));
+ merge_after_manifests.insert(merge_after_manifests.end(),
new_changes_manifests.begin(),
+ new_changes_manifests.end());
+ PAIMON_ASSIGN_OR_RAISE(delta_manifest_list,
manifest_list_->Write(new_changes_manifests));
+
+ PAIMON_ASSIGN_OR_RAISE(index_manifest_name,
index_manifest_file_->WriteIndexFiles(
+ old_index_manifest,
index_entries));
+
+ std::optional<std::pair<std::string, int64_t>> changelog_manifest_list;
+ std::optional<std::string> statistics;
+ int64_t changelog_record_count = 0;
+ int64_t schema_id = 0;
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>>
table_schema,
+ schema_manager_->Latest());
+ if (table_schema) {
+ schema_id = table_schema.value()->Id();
+ }
+
+ Snapshot new_snapshot(
+ new_snapshot_id, schema_id, base_manifest_list.first,
base_manifest_list.second,
+ delta_manifest_list.first, delta_manifest_list.second,
+ changelog_manifest_list ?
std::optional<std::string>(changelog_manifest_list.value().first)
+ : std::nullopt,
+ changelog_manifest_list ?
std::optional<int64_t>(changelog_manifest_list.value().second)
+ : std::nullopt,
+ index_manifest_name, commit_user_, identifier, commit_kind,
+ DateTimeUtils::GetCurrentUTCTimeUs() / 1000, log_offsets,
total_record_count,
+ delta_record_count, changelog_record_count, watermark, statistics,
+ properties.empty() ? std::nullopt
+ : std::optional<std::map<std::string,
std::string>>(properties),
+ next_row_id_start);
+
+ Result<bool> commit_result = CommitSnapshotImpl(new_snapshot,
delta_statistics);
+ if (!commit_result.ok()) {
+ // commit exception, not sure about the situation and should not clean
up the files.
+ PAIMON_LOG_WARN(logger_, "You need call FilterAndCommit to retry
commit for exception. %s",
+ commit_result.status().ToString().c_str());
+
+ // To prevent the case where an atomic write times out but actually
succeeds,
+ // retrying the commit could lead to the snapshot file being committed
multiple times.
+ // Therefore, retries should be handled by the upper layer,
+ // which should call FilterAndCommit to avoid duplicate commits.
+ // Therefore, we should not trigger cleanup here,
+ // as it may delete meta files from a snapshot that was just written
by ourselves,
+ // leading to an incomplete or corrupted snapshot.
+ guard.Release();
+ return Status::Invalid("You need call FilterAndCommit to retry commit
for exception. ",
+ commit_result.status().ToString());
+ }
+ bool commit_success = commit_result.value();
+ if (commit_success) {
+ PAIMON_LOG_INFO(logger_,
+ "Successfully commit snapshot %ld to table %s by user
%s with identifier "
+ "%ld and kind %s.",
+ new_snapshot.Id(), root_path_.c_str(),
commit_user_.c_str(),
+ new_snapshot.CommitIdentifier(),
+
Snapshot::CommitKind::ToString(new_snapshot.GetCommitKind()).c_str());
+ guard.Release();
+ return true;
+ } else {
+ // commit fails, should clean up the files
+ return false;
+ }
+}
+
+void FileStoreCommitImpl::AssignSnapshotId(int64_t snapshot_id,
+ std::vector<ManifestEntry>*
delta_files) const {
+ for (auto& entry : *delta_files) {
+ entry.AssignSequenceNumber(/*min_sequence_number=*/snapshot_id,
+ /*max_sequence_number=*/snapshot_id);
+ }
+}
+
+Result<int64_t> FileStoreCommitImpl::AssignRowTrackingMeta(
+ int64_t first_row_id_start, std::vector<ManifestEntry>* delta_files) const
{
+ if (delta_files->empty()) {
+ return first_row_id_start;
+ }
+ // assign row id for new files
+ int64_t start = first_row_id_start;
+ int64_t blob_start_default = first_row_id_start;
+ // Per-blob-field row id tracking: each blob field maintains its own start
position,
+ // keyed by the blob field name (from write_cols[0]).
+ std::map<std::string, int64_t> blob_starts;
+ // TODO(xinyu.lxy): support vector store file row tracking when vector
store is implemented
+ for (auto& entry : *delta_files) {
+ if (entry.File()->file_source == std::nullopt) {
+ return Status::Invalid(
+ "This is a bug, file source field for row-tracking table must
present.");
+ }
+ bool contains_row_id =
+ entry.File()->write_cols.has_value() &&
+ std::find(entry.File()->write_cols->begin(),
entry.File()->write_cols->end(),
+ SpecialFields::RowId().Name()) !=
entry.File()->write_cols->end();
+ if (entry.File()->file_source.value() == FileSource::Append() &&
+ entry.File()->first_row_id == std::nullopt && !contains_row_id) {
+ int64_t row_count = entry.File()->row_count;
+ if (BlobUtils::IsBlobFile(entry.File()->file_name)) {
+ // Use the first write_col as the blob field name to support
+ // independent row tracking per blob field.
+ std::string blob_field_name;
+ if (!entry.File()->write_cols ||
entry.File()->write_cols->empty()) {
+ return Status::Invalid(fmt::format(
+ "invalid blob file {}: does not have write_cols",
entry.File()->file_name));
+ }
+ blob_field_name = entry.File()->write_cols->at(0);
+ int64_t blob_start = blob_starts.count(blob_field_name)
+ ? blob_starts[blob_field_name]
+ : blob_start_default;
+ if (blob_start >= start) {
+ return Status::Invalid(fmt::format(
+ "This is a bug, blob start {} should be less than
start {} when "
+ "assigning a blob entry file.",
+ blob_start, start));
+ }
+ entry.AssignFirstRowId(blob_start);
+ blob_starts[blob_field_name] = blob_start + row_count;
+ } else {
+ entry.AssignFirstRowId(start);
+ blob_start_default = start;
+ blob_starts.clear();
+ start += row_count;
+ }
+ }
+ // for compact file, do not assign first row id.
+ }
+ return start;
+}
+
+Result<bool> FileStoreCommitImpl::CommitSnapshotImpl(
+ const Snapshot& new_snapshot, const std::vector<PartitionEntry>&
delta_statistics) {
+ std::vector<PartitionStatistics> statistics;
+ statistics.reserve(delta_statistics.size());
+ for (const auto& entry : delta_statistics) {
+ PAIMON_ASSIGN_OR_RAISE(PartitionStatistics partition_statistics,
+
entry.ToPartitionStatistics(partition_computer_.get()));
+ statistics.emplace_back(std::move(partition_statistics));
+ }
+ Result<bool> commit_result = snapshot_commit_->Commit(new_snapshot,
statistics);
+ if (!commit_result.ok()) {
+ // exception when performing the atomic rename,
+ // we cannot clean up because we can't determine the success
+ return Status::Invalid(fmt::format(
+ "Exception occurs when committing snapshot #{} by user {} with
identifier {} and kind "
+ "{}. Cannot clean up because we can't determine the success. {}",
+ new_snapshot.Id(), commit_user_, new_snapshot.CommitIdentifier(),
+ Snapshot::CommitKind::ToString(new_snapshot.GetCommitKind()),
+ commit_result.status().ToString()));
+ }
+ return commit_result;
+}
+
+void FileStoreCommitImpl::CleanUpTmpManifests(
+ const std::string& base_manifest_list_name, const std::string&
delta_manifest_list_name,
+ const std::vector<ManifestFileMeta>& merge_before_manifests,
+ const std::vector<ManifestFileMeta>& merge_after_manifests,
+ const std::optional<std::string>& old_index_manifest,
+ const std::optional<std::string>& new_index_manifest) {
+ if (!base_manifest_list_name.empty()) {
+ manifest_list_->DeleteQuietly(base_manifest_list_name);
+ PAIMON_LOG_DEBUG(logger_, "base manifest list %s",
base_manifest_list_name.c_str());
+ }
+ if (!delta_manifest_list_name.empty()) {
+ manifest_list_->DeleteQuietly(delta_manifest_list_name);
+ PAIMON_LOG_DEBUG(logger_, "delta manifest list %s",
delta_manifest_list_name.c_str());
+ }
+ // for faster searching
+ std::set<std::string> merge_before_manifest_set;
+ for (const auto& merge_before_manifest : merge_before_manifests) {
+ merge_before_manifest_set.emplace(merge_before_manifest.FileName());
+ }
+ // clean up newly merged manifest files
+ for (const auto& merge_after_manifest : merge_after_manifests) {
+ if (merge_before_manifest_set.find(merge_after_manifest.FileName()) ==
+ merge_before_manifest_set.end()) {
+ manifest_list_->DeleteQuietly(merge_after_manifest.FileName());
+ PAIMON_LOG_DEBUG(logger_, "delete new file %s",
+ merge_after_manifest.FileName().c_str());
+ }
+ }
+ // clean up index manifest
+ if (new_index_manifest && old_index_manifest != new_index_manifest) {
+ index_manifest_file_->DeleteQuietly(new_index_manifest.value());
+ PAIMON_LOG_DEBUG(logger_, "delete new index file %s",
new_index_manifest.value().c_str());
+ }
+}
+
+std::shared_ptr<ManifestCommittable>
FileStoreCommitImpl::CreateManifestCommittable(
+ int64_t identifier, const std::vector<std::shared_ptr<CommitMessage>>&
commit_messages,
+ std::optional<int64_t> watermark) {
+ auto committable = std::make_shared<ManifestCommittable>(identifier,
watermark);
+ for (const auto& commit_message : commit_messages) {
+ committable->AddFileCommittable(commit_message);
+ }
+ return committable;
+}
+
+Status FileStoreCommitImpl::CollectChanges(
+ const std::vector<std::shared_ptr<CommitMessage>>& commit_messages,
+ std::vector<ManifestEntry>* append_table_files,
+ std::vector<ManifestEntry>* append_changelog_files,
+ std::vector<ManifestEntry>* compact_table_files,
+ std::vector<ManifestEntry>* compact_changelog_files,
+ std::vector<IndexManifestEntry>* append_table_index_files,
+ std::vector<IndexManifestEntry>* compact_table_index_files) {
+ for (const auto& message : commit_messages) {
+ auto commit_message =
std::dynamic_pointer_cast<CommitMessageImpl>(message);
+ if (commit_message) {
+ DataIncrement new_files_increment =
commit_message->GetNewFilesIncrement();
+ for (const std::shared_ptr<DataFileMeta>& new_file :
new_files_increment.NewFiles()) {
+ append_table_files->push_back(MakeEntry(FileKind::Add(),
commit_message, new_file));
+ }
+ for (const std::shared_ptr<DataFileMeta>& deleted_file :
+ new_files_increment.DeletedFiles()) {
+ append_table_files->push_back(
+ MakeEntry(FileKind::Delete(), commit_message,
deleted_file));
+ }
+ for (const std::shared_ptr<DataFileMeta>& changelog_file :
+ new_files_increment.ChangelogFiles()) {
+ append_changelog_files->push_back(
+ MakeEntry(FileKind::Add(), commit_message,
changelog_file));
+ }
+ for (const std::shared_ptr<IndexFileMeta>& deleted_index_file :
+ new_files_increment.DeletedIndexFiles()) {
+ append_table_index_files->emplace_back(
+ FileKind::Delete(), commit_message->Partition(),
commit_message->Bucket(),
+ deleted_index_file);
+ }
+ for (const std::shared_ptr<IndexFileMeta>& new_index_file :
+ new_files_increment.NewIndexFiles()) {
+ append_table_index_files->emplace_back(FileKind::Add(),
commit_message->Partition(),
+
commit_message->Bucket(), new_index_file);
+ }
+ CompactIncrement compact_increment =
commit_message->GetCompactIncrement();
+ for (const std::shared_ptr<DataFileMeta>& compact_before :
+ compact_increment.CompactBefore()) {
+ compact_table_files->push_back(
+ MakeEntry(FileKind::Delete(), commit_message,
compact_before));
+ }
+ for (const std::shared_ptr<DataFileMeta>& compact_after :
+ compact_increment.CompactAfter()) {
+ compact_table_files->push_back(
+ MakeEntry(FileKind::Add(), commit_message, compact_after));
+ }
+ for (const std::shared_ptr<DataFileMeta>& changelog_file :
+ compact_increment.ChangelogFiles()) {
+ compact_changelog_files->push_back(
+ MakeEntry(FileKind::Add(), commit_message,
changelog_file));
+ }
+ for (const std::shared_ptr<IndexFileMeta>& deleted_index_file :
+ compact_increment.DeletedIndexFiles()) {
+ compact_table_index_files->emplace_back(
+ FileKind::Delete(), commit_message->Partition(),
commit_message->Bucket(),
+ deleted_index_file);
+ }
+ for (const std::shared_ptr<IndexFileMeta>& new_index_file :
+ compact_increment.NewIndexFiles()) {
+ compact_table_index_files->emplace_back(FileKind::Add(),
+
commit_message->Partition(),
+
commit_message->Bucket(), new_index_file);
+ }
+ } else {
+ return Status::Invalid("fail to cast commit message to commit
message impl");
+ }
+ }
+ return Status::OK();
+}
+
+ManifestEntry FileStoreCommitImpl::MakeEntry(
+ const FileKind& kind, const std::shared_ptr<CommitMessageImpl>&
commit_message,
+ const std::shared_ptr<DataFileMeta>& file) const {
+ int32_t total_buckets = commit_message->TotalBuckets() == std::nullopt
+ ? num_bucket_
+ : commit_message->TotalBuckets().value();
+ return ManifestEntry(kind, commit_message->Partition(),
commit_message->Bucket(), total_buckets,
+ file);
+}
+
+int64_t FileStoreCommitImpl::RowCounts(const std::vector<ManifestEntry>&
files) {
+ return std::accumulate(files.begin(), files.end(), 0L,
+ [](int64_t row_count, const ManifestEntry& entry) {
+ return row_count + entry.File()->row_count;
+ });
+}
+
+int64_t FileStoreCommitImpl::NumChangedPartitions(
+ const std::vector<std::vector<ManifestEntry>>& changes) {
+ std::unordered_set<BinaryRow> changed_partitions;
+ for (const auto& change : changes) {
+ for (const auto& entry : change) {
+ changed_partitions.insert(entry.Partition());
+ }
+ }
+ return static_cast<int64_t>(changed_partitions.size());
+}
+
+int64_t FileStoreCommitImpl::NumChangedBuckets(
+ const std::vector<std::vector<ManifestEntry>>& changes) {
+ std::unordered_map<BinaryRow, std::unordered_set<int>>
changed_partition_buckets;
+ for (const auto& change : changes) {
+ for (const auto& entry : change) {
+
changed_partition_buckets[entry.Partition()].insert(entry.Bucket());
+ }
+ }
+ return std::accumulate(changed_partition_buckets.begin(),
changed_partition_buckets.end(),
+ int64_t{0}, [](int64_t num_changed_buckets, const
auto& bucket) {
+ return num_changed_buckets +
+
static_cast<int64_t>(bucket.second.size());
+ });
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/file_store_commit_impl.h
b/src/paimon/core/operation/file_store_commit_impl.h
new file mode 100644
index 0000000..217edc0
--- /dev/null
+++ b/src/paimon/core/operation/file_store_commit_impl.h
@@ -0,0 +1,236 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "paimon/common/options/memory_size.h"
+#include "paimon/core/catalog/snapshot_commit.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/manifest/partition_entry.h"
+#include "paimon/core/snapshot.h"
+#include "paimon/file_store_commit.h"
+#include "paimon/logging.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/metrics.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class DataType;
+class Schema;
+} // namespace arrow
+
+namespace paimon {
+
+class CommitContext;
+class CommitMessageImpl;
+struct DataFileMeta;
+class ExpireSnapshots;
+class FileKind;
+class FileStorePathFactory;
+class ManifestEntry;
+class ManifestCommittable;
+class ManifestFile;
+class IndexManifestFile;
+struct IndexManifestEntry;
+class ManifestList;
+class ManifestFileMeta;
+class SnapshotManager;
+class SchemaManager;
+class TableSchema;
+class BinaryRowPartitionComputer;
+class CommitMessage;
+class Executor;
+class FileSystem;
+class Logger;
+class MemoryPool;
+class Metrics;
+class PartitionEntry;
+class SnapshotCommit;
+
+/// Commit operation which provides commit and overwrite.
+class FileStoreCommitImpl : public FileStoreCommit {
+ public:
+ FileStoreCommitImpl(const std::shared_ptr<MemoryPool>& pool,
+ const std::shared_ptr<Executor>& executor,
+ const std::shared_ptr<arrow::Schema>& schema, const
std::string& root_path,
+ const std::string& commit_user, const CoreOptions&
options,
+ const std::shared_ptr<FileStorePathFactory>&
path_factory,
+ std::unique_ptr<BinaryRowPartitionComputer>
partition_computer,
+ const std::shared_ptr<SnapshotManager>&
snapshot_manager,
+ bool ignore_empty_commit, bool use_rest_catalog_commit,
+ const std::shared_ptr<TableSchema>& table_schema,
+ const std::shared_ptr<ManifestFile>& manifest_file,
+ const std::shared_ptr<ManifestList>& manifest_list,
+ const std::shared_ptr<IndexManifestFile>&
index_manifest_file,
+ const std::shared_ptr<ExpireSnapshots>&
expire_snapshots,
+ const std::shared_ptr<SchemaManager>& schema_manager);
+ ~FileStoreCommitImpl() override;
+
+ Status Commit(const std::vector<std::shared_ptr<CommitMessage>>&
commit_messages,
+ int64_t commit_identifier,
+ std::optional<int64_t> watermark = std::nullopt) override;
+
+ Result<int32_t> FilterAndCommit(
+ const std::map<int64_t, std::vector<std::shared_ptr<CommitMessage>>>&
+ commit_identifier_and_messages,
+ std::optional<int64_t> watermark = std::nullopt) override;
+
+ Status Overwrite(const std::vector<std::map<std::string, std::string>>&
partitions,
+ const std::vector<std::shared_ptr<CommitMessage>>&
commit_messages,
+ int64_t commit_identifier,
+ std::optional<int64_t> watermark = std::nullopt) override;
+
+ Result<int32_t> FilterAndOverwrite(
+ const std::vector<std::map<std::string, std::string>>& partitions,
+ const std::vector<std::shared_ptr<CommitMessage>>& commit_messages,
+ int64_t commit_identifier, std::optional<int64_t> watermark =
std::nullopt) override;
+
+ Result<std::string> GetLastCommitTableRequest() override;
+
+ Result<int32_t> Expire() override;
+
+ Status DropPartition(const std::vector<std::map<std::string,
std::string>>& partitions,
+ int64_t commit_identifier) override;
+
+ std::shared_ptr<Metrics> GetCommitMetrics() const override {
+ return metrics_;
+ }
+
+ Status Init(std::unique_ptr<CommitContext> ctx);
+
+ private:
+ Status Commit(const std::shared_ptr<ManifestCommittable>&
manifest_committable,
+ bool check_append_files);
+
+ Status TryOverwrite(const std::vector<std::map<std::string, std::string>>&
partition,
+ const std::vector<ManifestEntry>& changes, int64_t
commit_identifier,
+ std::optional<int64_t> watermark);
+
+ Result<std::vector<ManifestEntry>> GetAllFiles(
+ const Snapshot& snapshot,
+ const std::vector<std::map<std::string, std::string>>& partitions);
+
+ Result<std::vector<std::shared_ptr<ManifestCommittable>>> FilterCommitted(
+ const std::vector<std::shared_ptr<ManifestCommittable>>& committables);
+
+ std::shared_ptr<ManifestCommittable> CreateManifestCommittable(
+ int64_t identifier, const std::vector<std::shared_ptr<CommitMessage>>&
commit_messages,
+ std::optional<int64_t> watermark);
+
+ ManifestEntry MakeEntry(const FileKind& kind,
+ const std::shared_ptr<CommitMessageImpl>&
commit_message,
+ const std::shared_ptr<DataFileMeta>& file) const;
+
+ Status CollectChanges(const std::vector<std::shared_ptr<CommitMessage>>&
commit_messages,
+ std::vector<ManifestEntry>* append_table_files,
+ std::vector<ManifestEntry>* append_changelog_files,
+ std::vector<ManifestEntry>* compact_table_files,
+ std::vector<ManifestEntry>* compact_changelog_files,
+ std::vector<IndexManifestEntry>*
append_table_index_files,
+ std::vector<IndexManifestEntry>*
compact_table_index_files);
+
+ Result<int32_t> TryCommit(const std::vector<ManifestEntry>& delta_files,
+ const std::vector<IndexManifestEntry>&
index_entries,
+ int64_t identifier, std::optional<int64_t>
watermark,
+ std::map<int32_t, int64_t> log_offsets,
+ const std::map<std::string, std::string>&
properties,
+ Snapshot::CommitKind commit_kind, bool
check_append_files);
+ Result<bool> TryCommitOnce(const std::vector<ManifestEntry>& delta_files,
+ const std::vector<IndexManifestEntry>&
index_entries,
+ int64_t commit_identifier,
std::optional<int64_t> watermark,
+ std::map<int32_t, int64_t> log_offsets,
+ const std::map<std::string, std::string>&
properties,
+ Snapshot::CommitKind commit_kind,
+ const std::optional<Snapshot>& latest_snapshot,
+ bool need_conflict_check);
+
+ Result<bool> CommitSnapshotImpl(const Snapshot& new_snapshot,
+ const std::vector<PartitionEntry>&
delta_statistics);
+
+ void CleanUpTmpManifests(const std::string& previous_changes_list_name,
+ const std::string& new_changes_list_name,
+ const std::vector<ManifestFileMeta>& old_metas,
+ const std::vector<ManifestFileMeta>& new_metas,
+ const std::optional<std::string>&
old_index_manifest,
+ const std::optional<std::string>&
new_index_manifest);
+
+ Result<std::vector<ManifestEntry>> ReadAllEntriesFromChangedPartitions(
+ const Snapshot& latest_snapshot,
+ const std::set<std::map<std::string, std::string>>& partitions) const;
+
+ Status NoConflictsOrFail(const std::string& base_commit_user,
+ const std::vector<ManifestEntry>& base_entries,
+ const std::vector<ManifestEntry>& changes) const;
+
+ Status CheckFilesExistence(
+ const std::vector<std::shared_ptr<ManifestCommittable>>& committables)
const;
+
+ void AssignSnapshotId(int64_t snapshot_id, std::vector<ManifestEntry>*
delta_files) const;
+
+ Result<int64_t> AssignRowTrackingMeta(int64_t first_row_id_start,
+ std::vector<ManifestEntry>*
delta_files) const;
+
+ Result<std::set<std::map<std::string, std::string>>> ChangedPartitions(
+ const std::vector<ManifestEntry>& data_files,
+ const std::vector<IndexManifestEntry>& index_files) const;
+
+ static int64_t RowCounts(const std::vector<ManifestEntry>& files);
+
+ static int64_t NumChangedPartitions(const
std::vector<std::vector<ManifestEntry>>& changes);
+
+ static int64_t NumChangedBuckets(const
std::vector<std::vector<ManifestEntry>>& changes);
+
+ private:
+ std::shared_ptr<MemoryPool> memory_pool_;
+ std::shared_ptr<Executor> executor_;
+ std::shared_ptr<arrow::Schema> schema_;
+ std::string root_path_;
+ std::string commit_user_;
+ CoreOptions options_;
+ std::shared_ptr<FileStorePathFactory> path_factory_;
+ std::shared_ptr<FileSystem> fs_;
+
+ std::unique_ptr<BinaryRowPartitionComputer> partition_computer_;
+ std::shared_ptr<SnapshotManager> snapshot_manager_;
+ std::shared_ptr<SnapshotCommit> snapshot_commit_;
+ bool ignore_empty_commit_ = true;
+ int32_t num_bucket_ = 0;
+ std::shared_ptr<TableSchema> table_schema_;
+
+ std::shared_ptr<ManifestFile> manifest_file_;
+ std::shared_ptr<ManifestList> manifest_list_;
+ std::shared_ptr<IndexManifestFile> index_manifest_file_;
+
+ std::shared_ptr<ExpireSnapshots> expire_snapshots_;
+ std::shared_ptr<SchemaManager> schema_manager_;
+
+ std::shared_ptr<Metrics> metrics_;
+ std::shared_ptr<Logger> logger_;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/file_store_commit_test.cpp
b/src/paimon/core/operation/file_store_commit_test.cpp
new file mode 100644
index 0000000..3ee13c9
--- /dev/null
+++ b/src/paimon/core/operation/file_store_commit_test.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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/file_store_commit.h"
+
+#include <utility>
+
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
+#include "arrow/status.h"
+#include "arrow/type.h"
+#include "gtest/gtest.h"
+#include "paimon/catalog/catalog.h"
+#include "paimon/catalog/identifier.h"
+#include "paimon/commit_context.h"
+#include "paimon/common/utils/path_util.h"
+#include "paimon/core/operation/file_store_commit_impl.h"
+#include "paimon/defs.h"
+#include "paimon/result.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(FileStoreCommitTest, TestCreate) {
+ auto string_field = arrow::field("f0", arrow::utf8());
+ auto int_field = arrow::field("f1", arrow::int32());
+ auto int_field1 = arrow::field("f2", arrow::int32());
+ auto double_field = arrow::field("f3", arrow::float64());
+ auto schema =
+ arrow::schema(arrow::FieldVector({string_field, int_field, int_field1,
double_field}));
+
+ ::ArrowSchema arrow_schema;
+ ASSERT_TRUE(arrow::ExportSchema(*schema, &arrow_schema).ok());
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+
+ std::map<std::string, std::string> options = {{Options::FILE_FORMAT,
"orc"},
+ {Options::TARGET_FILE_SIZE,
"1024"},
+ {Options::FILE_SYSTEM,
"local"},
+ {Options::BUCKET, "2"},
+ {Options::BUCKET_KEY, "f2"}};
+
+ ASSERT_OK_AND_ASSIGN(auto catalog, Catalog::Create(dir->Str(), options));
+ ASSERT_OK(catalog->CreateDatabase("foo", options,
/*ignore_if_exists=*/false));
+ ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar"), &arrow_schema,
+ /*partition_keys=*/{"f1"},
+ /*primary_keys=*/{}, options,
+ /*ignore_if_exists=*/false));
+ std::string table_path = PathUtil::JoinPath(dir->Str(), "foo.db/bar");
+
+ CommitContextBuilder context_builder(table_path, "commit_user");
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context,
+ context_builder.AddOption(Options::MANIFEST_FORMAT,
"orc")
+ .AddOption(Options::MANIFEST_TARGET_FILE_SIZE,
"8mb")
+ .AddOption(Options::FILE_SYSTEM, "local")
+ .Finish());
+ ASSERT_OK_AND_ASSIGN(auto commit,
FileStoreCommit::Create(std::move(commit_context)));
+ auto commit_impl = dynamic_cast<FileStoreCommitImpl*>(commit.get());
+ ASSERT_TRUE(commit_impl);
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/core/operation/manifest_file_merger.cpp
b/src/paimon/core/operation/manifest_file_merger.cpp
new file mode 100644
index 0000000..aa308e0
--- /dev/null
+++ b/src/paimon/core/operation/manifest_file_merger.cpp
@@ -0,0 +1,185 @@
+/*
+ * 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/manifest_file_merger.h"
+
+#include <cstddef>
+#include <functional>
+#include <list>
+#include <string>
+#include <utility>
+
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/manifest/file_entry.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/manifest/manifest_file.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+std::shared_ptr<Logger> ManifestFileMerger::GetLogger() {
+ static std::shared_ptr<Logger> logger =
Logger::GetLogger("ManifestFileMerger");
+ return logger;
+}
+
+Result<std::vector<ManifestFileMeta>> ManifestFileMerger::Merge(
+ const std::vector<ManifestFileMeta>& all, int64_t target_file_size,
int32_t merge_min_count,
+ int64_t full_compaction_file_size, ManifestFile* manifest_file) {
+ if (manifest_file == nullptr) {
+ return Status::Invalid("manifest_file is null pointer");
+ }
+
+ // these are the newly created manifest files, clean them up if exception
occurs
+ std::vector<ManifestFileMeta> new_metas_for_abort;
+ ScopeGuard guard([&]() {
+ for (const auto& meta : new_metas_for_abort) {
+ manifest_file->DeleteQuietly(meta.FileName());
+ }
+ });
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::vector<ManifestFileMeta>>
full_compacted,
+ TryFullCompaction(all, target_file_size,
full_compaction_file_size,
+ manifest_file,
&new_metas_for_abort));
+ std::vector<ManifestFileMeta> results;
+ if (full_compacted != std::nullopt) {
+ results = full_compacted.value();
+ } else {
+ PAIMON_ASSIGN_OR_RAISE(results, TryMinorCompaction(all,
target_file_size, merge_min_count,
+ manifest_file,
&new_metas_for_abort));
+ }
+ guard.Release();
+ return results;
+}
+
+// TryFullCompaction aims to perform a full compaction of manifest files. It
consolidates
+// manifest files into two categories, base and delta, and applies a "full
compaction" condition
+// if certain thresholds are met.
+Result<std::optional<std::vector<ManifestFileMeta>>>
ManifestFileMerger::TryFullCompaction(
+ const std::vector<ManifestFileMeta>& all, int64_t target_file_size,
+ int64_t full_compaction_file_size, ManifestFile* manifest_file,
+ std::vector<ManifestFileMeta>* new_metas_for_abort) {
+ // 1. Splitting base and delta sets
+ std::vector<ManifestFileMeta> base;
+ int64_t total_manifest_size = 0;
+ size_t i = 0;
+ for (; i < all.size(); i++) {
+ ManifestFileMeta file = all[i];
+ if (file.NumDeletedFiles() == 0 && file.FileSize() >=
target_file_size) {
+ base.push_back(file);
+ total_manifest_size += file.FileSize();
+ } else {
+ break;
+ }
+ }
+ std::vector<ManifestFileMeta> delta;
+ int64_t delta_delete_file_num = 0;
+ int64_t total_delta_file_size = 0;
+ for (; i < all.size(); i++) {
+ const ManifestFileMeta& file = all[i];
+ delta.push_back(file);
+ total_manifest_size += file.FileSize();
+ total_delta_file_size += file.FileSize();
+ delta_delete_file_num += file.NumDeletedFiles();
+ }
+ // 2. Determining Full Compaction Requirement
+ if (total_delta_file_size < full_compaction_file_size) {
+ return std::optional<std::vector<ManifestFileMeta>>();
+ }
+
+ // 3. Merging Delta Files
+ PAIMON_LOG_DEBUG(GetLogger(),
+ "Start Manifest File Full Compaction, pick the number of
delete file: %ld, "
+ "total manifest file size: %ld",
+ delta_delete_file_num, total_manifest_size);
+
+ if (delta.size() <= 1) {
+ return std::optional<std::vector<ManifestFileMeta>>();
+ }
+
+ PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestFileMeta> merged_delta,
+ MergeEntries(delta, manifest_file));
+
+ // 4. Result Construction and Return
+ std::vector<ManifestFileMeta> result;
+ result.insert(result.end(), base.begin(), base.end());
+ result.insert(result.end(), merged_delta.begin(), merged_delta.end());
+ new_metas_for_abort->insert(new_metas_for_abort->end(),
merged_delta.begin(),
+ merged_delta.end());
+ return std::optional<std::vector<ManifestFileMeta>>(result);
+}
+
+// TryMinorCompaction focuses on performing a "minor compaction" of manifest
files, especially
+// those that are smaller than the suggested meta file size.
+Result<std::vector<ManifestFileMeta>> ManifestFileMerger::TryMinorCompaction(
+ const std::vector<ManifestFileMeta>& input, int64_t suggested_meta_size,
+ int32_t suggested_min_meta_count, ManifestFile* manifest_file,
+ std::vector<ManifestFileMeta>* new_metas_for_abort) {
+ std::vector<ManifestFileMeta> result;
+ std::vector<ManifestFileMeta> candidates;
+ int64_t total_size = 0;
+ // merge existing small manifest files
+ for (const ManifestFileMeta& manifest : input) {
+ candidates.push_back(manifest);
+ total_size += manifest.FileSize();
+ if (total_size >= suggested_meta_size) {
+ if (candidates.size() == 1) {
+ result.push_back(candidates[0]);
+ } else {
+ // reach suggested file size, perform merging and produce new
file
+ PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestFileMeta> merged,
+ MergeEntries(candidates,
manifest_file));
+ result.insert(result.end(), merged.begin(), merged.end());
+ new_metas_for_abort->insert(new_metas_for_abort->end(),
merged.begin(),
+ merged.end());
+ }
+ candidates.clear();
+ total_size = 0;
+ }
+ }
+
+ // merge the last bit of manifests if there are too many
+ if (candidates.size() >= static_cast<uint32_t>(suggested_min_meta_count)) {
+ if (candidates.size() == 1) {
+ result.push_back(candidates[0]);
+ } else {
+ PAIMON_ASSIGN_OR_RAISE(std::vector<ManifestFileMeta> merged,
+ MergeEntries(candidates, manifest_file));
+ result.insert(result.end(), merged.begin(), merged.end());
+ new_metas_for_abort->insert(new_metas_for_abort->end(),
merged.begin(), merged.end());
+ }
+ } else {
+ result.insert(result.end(), candidates.begin(), candidates.end());
+ }
+ return result;
+}
+
+Result<std::vector<ManifestFileMeta>> ManifestFileMerger::MergeEntries(
+ const std::vector<ManifestFileMeta>& metas, ManifestFile* manifest_file) {
+ if (metas.size() == 1) {
+ return std::vector<ManifestFileMeta>({metas[0]});
+ }
+ std::vector<ManifestEntry> entries;
+ for (const auto& meta : metas) {
+ PAIMON_RETURN_NOT_OK(manifest_file->Read(meta.FileName(),
/*filter=*/nullptr, &entries));
+ }
+ std::vector<ManifestEntry> result;
+ PAIMON_RETURN_NOT_OK(FileEntry::MergeEntries<ManifestEntry>(entries,
&result));
+ return manifest_file->Write(result);
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/manifest_file_merger.h
b/src/paimon/core/operation/manifest_file_merger.h
new file mode 100644
index 0000000..0cc33a7
--- /dev/null
+++ b/src/paimon/core/operation/manifest_file_merger.h
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <unordered_map>
+#include <vector>
+
+#include "paimon/core/manifest/file_entry.h"
+#include "paimon/core/manifest/manifest_file_meta.h"
+#include "paimon/logging.h"
+#include "paimon/result.h"
+
+namespace arrow {
+class DataType;
+} // namespace arrow
+
+namespace paimon {
+
+class ManifestFile;
+
+/// This file includes several `ManifestFileMeta`, representing all data of
the whole
+/// table at the corresponding snapshot.
+class ManifestFileMerger {
+ public:
+ /// Merge several `ManifestFileMeta`s.
+ ///
+ /// @note This method is atomic.
+ static Result<std::vector<ManifestFileMeta>> Merge(const
std::vector<ManifestFileMeta>& all,
+ int64_t
manifest_target_file_size,
+ int32_t merge_min_count,
+ int64_t
full_compaction_file_size,
+ ManifestFile*
manifest_file);
+
+ private:
+ static Result<std::optional<std::vector<ManifestFileMeta>>>
TryFullCompaction(
+ const std::vector<ManifestFileMeta>& all, int64_t
manifest_target_file_size,
+ int64_t full_compaction_file_size, ManifestFile* manifest_file,
+ std::vector<ManifestFileMeta>* new_metas);
+
+ static Result<std::vector<ManifestFileMeta>> TryMinorCompaction(
+ const std::vector<ManifestFileMeta>& all, int64_t
manifest_target_file_size,
+ int32_t merge_min_count, ManifestFile* manifest_file,
+ std::vector<ManifestFileMeta>* new_metas);
+
+ static Result<std::vector<ManifestFileMeta>> MergeEntries(
+ const std::vector<ManifestFileMeta>& metas, ManifestFile*
manifest_file);
+
+ static std::shared_ptr<Logger> GetLogger();
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/manifest_file_merger_test.cpp
b/src/paimon/core/operation/manifest_file_merger_test.cpp
new file mode 100644
index 0000000..150723f
--- /dev/null
+++ b/src/paimon/core/operation/manifest_file_merger_test.cpp
@@ -0,0 +1,375 @@
+/*
+ * 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/manifest_file_merger.h"
+
+#include <cassert>
+#include <functional>
+#include <limits>
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "arrow/type.h"
+#include "fmt/format.h"
+#include "gtest/gtest.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/data/binary_row_writer.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/core/manifest/file_source.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/manifest/manifest_file.h"
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/core/utils/field_mapping.h"
+#include "paimon/core/utils/file_store_path_factory.h"
+#include "paimon/data/timestamp.h"
+#include "paimon/format/file_format.h"
+#include "paimon/format/file_format_factory.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+class ManifestFileMergerTest : public testing::Test {
+ public:
+ void SetUp() override {
+ pool_ = GetDefaultPool();
+ dir_ = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir_);
+ test_root_ = dir_->Str();
+ partition_type_ = arrow::int64();
+ CreateManifestFile(test_root_);
+ assert(manifest_file_);
+ }
+
+ void TearDown() override {}
+
+ ManifestEntry MakeEntry(FileKind kind, const std::string& file_name) {
+ return MakeEntry(kind, file_name, 0);
+ }
+
+ ManifestEntry MakeEntry(FileKind kind, const std::string& file_name,
+ std::optional<int32_t> partition) {
+ BinaryRow binary_row = BinaryRow::EmptyRow();
+ if (partition != std::nullopt) {
+ binary_row = BinaryRow(1);
+ BinaryRowWriter writer(&binary_row, 0, pool_.get());
+ writer.WriteInt(0, partition.value());
+ writer.Complete();
+ }
+
+ return ManifestEntry(
+ kind, binary_row,
+ 0, // not used
+ 0, // not used
+ std::make_shared<DataFileMeta>(
+ file_name,
+ 0, // not used
+ 0, // not used
+ binary_row, // not used
+ binary_row, // not used
+ SimpleStats::EmptyStats(), // not used
+ SimpleStats::EmptyStats(), // not used
+ 0, // not used
+ 0, // not used
+ 0, // not used
+ 0, // not used
+ /*extra_files=*/std::vector<std::optional<std::string>>(),
Timestamp(200000, 0),
+ 0, // not used
+ nullptr, // not used
+ FileSource::Append(), /*value_stats_cols=*/std::nullopt,
+ /*external_path=*/std::nullopt, /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt));
+ }
+
+ ManifestFileMeta MakeManifest(const std::vector<ManifestEntry>& entries) {
+ EXPECT_OK_AND_ASSIGN(std::vector<ManifestFileMeta> manifest_file_metas,
+ manifest_file_->Write(entries));
+ // force the file size of each manifest to be 3000
+ manifest_file_metas[0].file_size_ = 3000;
+ return manifest_file_metas[0];
+ }
+
+ private:
+ void CreateManifestFile(const std::string& path_str) {
+ auto file_system = std::make_shared<LocalFileSystem>();
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<FileFormat> file_format,
+ FileFormatFactory::Get("parquet", std::map<std::string,
std::string>()));
+ auto schema = arrow::schema(arrow::FieldVector({arrow::field("f0",
partition_type_)}));
+ ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap({}));
+ ASSERT_OK_AND_ASSIGN(std::vector<std::string> external_paths,
+ options.CreateExternalPaths());
+ ASSERT_OK_AND_ASSIGN(std::optional<std::string>
global_index_external_path,
+ options.CreateGlobalIndexExternalPath());
+
+ ASSERT_OK_AND_ASSIGN(
+ static std::shared_ptr<FileStorePathFactory> path_factory,
+ FileStorePathFactory::Create(
+ path_str, schema, /*partition_keys=*/{"f0"},
options.GetPartitionDefaultName(),
+ options.GetFileFormat()->Identifier(),
options.DataFilePrefix(),
+ options.LegacyPartitionNameEnabled(), external_paths,
global_index_external_path,
+ options.IndexFileInDataFileDir(), pool_));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Schema> partition_schema,
+ FieldMapping::GetPartitionSchema(schema, {"f0"}));
+ ASSERT_OK_AND_ASSIGN(manifest_file_,
+ ManifestFile::Create(file_system, file_format,
"zstd", path_factory,
+ /*target_file_size=*/1024 *
1024, pool_, options,
+ partition_schema));
+ }
+
+ void ContainSameEntryFile(
+ const std::vector<ManifestFileMeta>& metas,
+ const std::vector<std::pair<std::string, FileKind>>&
entry_file_name_expected) {
+ std::vector<ManifestEntry> entries;
+ for (const auto& meta : metas) {
+ ASSERT_OK(manifest_file_->Read(meta.FileName(),
/*filter=*/nullptr, &entries));
+ }
+ std::vector<std::pair<std::string, FileKind>> entry_file_name_actual;
+ for (const auto& entry : entries) {
+ entry_file_name_actual.emplace_back(entry.FileName(),
entry.Kind());
+ }
+ ASSERT_EQ(entry_file_name_expected, entry_file_name_actual);
+ }
+
+ void AssertEquivalentEntries(const std::vector<ManifestFileMeta>& lhs,
+ const std::vector<ManifestFileMeta>& rhs) {
+ ASSERT_EQ(lhs.size(), rhs.size());
+ for (uint32_t i = 0; i < lhs.size(); i++) {
+ ASSERT_EQ(lhs[i].ToString(), rhs[i].ToString());
+ }
+ }
+
+ static constexpr int64_t MAX_LONG_VALUE =
std::numeric_limits<int64_t>::max();
+ static constexpr int32_t MAX_INT_VALUE =
std::numeric_limits<int32_t>::max();
+
+ std::unique_ptr<UniqueTestDirectory> dir_;
+ std::string test_root_;
+ std::shared_ptr<MemoryPool> pool_;
+ std::shared_ptr<ManifestFile> manifest_file_;
+ std::shared_ptr<arrow::DataType> partition_type_;
+};
+
+TEST_F(ManifestFileMergerTest, TestMergeWithoutCompaction) {
+ std::vector<ManifestEntry> entries;
+ for (int32_t i = 0; i < 16; i++) {
+ entries.push_back(MakeEntry(FileKind::Add(), std::to_string(i)));
+ }
+ std::vector<ManifestFileMeta> input;
+ // base manifest
+ input.push_back(MakeManifest(entries));
+
+ // delta manifest
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "A"),
MakeEntry(FileKind::Add(), "B"),
+ MakeEntry(FileKind::Add(), "C")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "D")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "A"),
MakeEntry(FileKind::Delete(), "B"),
+ MakeEntry(FileKind::Add(), "F")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "14"),
MakeEntry(FileKind::Delete(), "15")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "C"),
MakeEntry(FileKind::Delete(), "D"),
+ MakeEntry(FileKind::Delete(), "F"),
MakeEntry(FileKind::Add(), "G")}));
+ // every file is larger than target manifest file size 500
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<ManifestFileMeta> actual,
+ ManifestFileMerger::Merge(input, 500, 3, MAX_LONG_VALUE,
manifest_file_.get()));
+ AssertEquivalentEntries(input, actual);
+}
+
+TEST_F(ManifestFileMergerTest, TestMergeWithoutDeleteFile) {
+ // entries are All Add().
+ std::vector<ManifestFileMeta> input;
+ // base
+ for (int32_t j = 0; j < 6; j++) {
+ std::vector<ManifestEntry> entries;
+ for (int32_t i = 1; i < 16; i++) {
+ entries.push_back(MakeEntry(FileKind::Add(), fmt::format("{}-{}",
j, i), j));
+ }
+ input.push_back(MakeManifest(entries));
+ }
+ // delta
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "A")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "B")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "C")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "D")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "E")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "F")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "G")}));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<ManifestFileMeta> merged,
+ ManifestFileMerger::Merge(input, 500, 3, 200,
manifest_file_.get()));
+ AssertEquivalentEntries(input, merged);
+}
+
+TEST_F(ManifestFileMergerTest, TestTriggerMinorCompaction) {
+ std::vector<ManifestEntry> entries;
+ for (int32_t i = 0; i < 16; i++) {
+ entries.push_back(MakeEntry(FileKind::Add(), std::to_string(i)));
+ }
+ std::vector<ManifestFileMeta> input;
+ // base manifest
+ input.push_back(MakeManifest(entries));
+
+ // delta manifest
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "A"),
MakeEntry(FileKind::Add(), "B"),
+ MakeEntry(FileKind::Add(), "C")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "D")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "A"),
MakeEntry(FileKind::Delete(), "B"),
+ MakeEntry(FileKind::Add(), "F")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "14"),
MakeEntry(FileKind::Delete(), "15")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "C"),
MakeEntry(FileKind::Delete(), "D"),
+ MakeEntry(FileKind::Delete(), "F"),
MakeEntry(FileKind::Add(), "G")}));
+
+ std::vector<ManifestFileMeta> new_metas;
+ // trigger minor compaction
+ ASSERT_OK_AND_ASSIGN(std::vector<ManifestFileMeta> merged,
+ ManifestFileMerger::TryMinorCompaction(
+ input, /*manifest_target_file_size=*/5000,
+ /*merge_min_count=*/30, manifest_file_.get(),
&new_metas));
+ ASSERT_EQ(3, new_metas.size());
+
+ std::vector<std::pair<std::string, FileKind>> entry_file_expected;
+ for (int32_t i = 0; i < 16; i++) {
+ entry_file_expected.emplace_back(std::to_string(i), FileKind::Add());
+ }
+ entry_file_expected.emplace_back("A", FileKind::Add());
+ entry_file_expected.emplace_back("B", FileKind::Add());
+ entry_file_expected.emplace_back("C", FileKind::Add());
+ entry_file_expected.emplace_back("D", FileKind::Add());
+ entry_file_expected.emplace_back("A", FileKind::Delete());
+ entry_file_expected.emplace_back("B", FileKind::Delete());
+ entry_file_expected.emplace_back("F", FileKind::Add());
+ entry_file_expected.emplace_back("14", FileKind::Delete());
+ entry_file_expected.emplace_back("15", FileKind::Delete());
+ entry_file_expected.emplace_back("C", FileKind::Delete());
+ entry_file_expected.emplace_back("D", FileKind::Delete());
+ entry_file_expected.emplace_back("F", FileKind::Delete());
+ entry_file_expected.emplace_back("G", FileKind::Add());
+ ContainSameEntryFile(merged, entry_file_expected);
+}
+
+TEST_F(ManifestFileMergerTest, TestTriggerMinorCompactionWithLastBit) {
+ std::vector<ManifestEntry> entries;
+ for (int32_t i = 0; i < 16; i++) {
+ entries.push_back(MakeEntry(FileKind::Add(), std::to_string(i)));
+ }
+ std::vector<ManifestFileMeta> input;
+ // base manifest
+ input.push_back(MakeManifest(entries));
+
+ // delta manifest
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "A"),
MakeEntry(FileKind::Add(), "B"),
+ MakeEntry(FileKind::Add(), "C")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "D")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "A"),
MakeEntry(FileKind::Delete(), "B"),
+ MakeEntry(FileKind::Add(), "F")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "14"),
MakeEntry(FileKind::Delete(), "15")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "C"),
MakeEntry(FileKind::Delete(), "D"),
+ MakeEntry(FileKind::Delete(), "F"),
MakeEntry(FileKind::Add(), "G")}));
+
+ std::vector<ManifestFileMeta> new_metas;
+ // trigger minor compaction
+ ASSERT_OK_AND_ASSIGN(std::vector<ManifestFileMeta> merged,
+ ManifestFileMerger::TryMinorCompaction(
+ input, /*manifest_target_file_size=*/10000,
+ /*merge_min_count=*/2, manifest_file_.get(),
&new_metas));
+ ASSERT_EQ(2, new_metas.size());
+
+ std::vector<std::pair<std::string, FileKind>> entry_file_expected;
+ for (int32_t i = 0; i < 16; i++) {
+ entry_file_expected.emplace_back(std::to_string(i), FileKind::Add());
+ }
+ entry_file_expected.emplace_back("C", FileKind::Add());
+ entry_file_expected.emplace_back("D", FileKind::Add());
+ entry_file_expected.emplace_back("F", FileKind::Add());
+
+ entry_file_expected.emplace_back("14", FileKind::Delete());
+ entry_file_expected.emplace_back("15", FileKind::Delete());
+ entry_file_expected.emplace_back("C", FileKind::Delete());
+ entry_file_expected.emplace_back("D", FileKind::Delete());
+ entry_file_expected.emplace_back("F", FileKind::Delete());
+ entry_file_expected.emplace_back("G", FileKind::Add());
+ ContainSameEntryFile(merged, entry_file_expected);
+}
+
+TEST_F(ManifestFileMergerTest, TestTriggerFullCompaction) {
+ std::vector<ManifestEntry> entries;
+ for (int32_t i = 0; i < 16; i++) {
+ entries.push_back(MakeEntry(FileKind::Add(), std::to_string(i)));
+ }
+
+ std::vector<ManifestFileMeta> input;
+
+ // base manifest
+ input.push_back(MakeManifest(entries));
+
+ // delta manifest
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "A"),
MakeEntry(FileKind::Add(), "B"),
+ MakeEntry(FileKind::Add(), "C")}));
+ input.push_back(MakeManifest({MakeEntry(FileKind::Add(), "D")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "A"),
MakeEntry(FileKind::Delete(), "B"),
+ MakeEntry(FileKind::Add(), "F")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "14"),
MakeEntry(FileKind::Delete(), "15")}));
+ input.push_back(
+ MakeManifest({MakeEntry(FileKind::Delete(), "C"),
MakeEntry(FileKind::Delete(), "D"),
+ MakeEntry(FileKind::Delete(), "F"),
MakeEntry(FileKind::Add(), "G")}));
+
+ // no trigger for delta size
+ std::vector<ManifestFileMeta> new_metas;
+ ASSERT_OK_AND_ASSIGN(
+ std::optional<std::vector<ManifestFileMeta>> full_compacted,
+ ManifestFileMerger::TryFullCompaction(input,
/*manifest_target_file_size=*/500,
+
/*full_compaction_file_size=*/MAX_INT_VALUE,
+ manifest_file_.get(),
&new_metas));
+ ASSERT_EQ(std::nullopt, full_compacted);
+ ASSERT_EQ(0, new_metas.size());
+ new_metas.clear();
+
+ // trigger full compaction
+ ASSERT_OK_AND_ASSIGN(std::optional<std::vector<ManifestFileMeta>> merged,
+ ManifestFileMerger::TryFullCompaction(
+ input, /*manifest_target_file_size=*/5000,
+ /*full_compaction_file_size=*/100,
manifest_file_.get(), &new_metas));
+ ASSERT_NE(std::nullopt, merged);
+ ASSERT_GT(new_metas.size(), 0);
+
+ std::vector<std::pair<std::string, FileKind>> entry_file_expected;
+ for (int32_t i = 0; i < 14; i++) {
+ entry_file_expected.emplace_back(std::to_string(i), FileKind::Add());
+ }
+ entry_file_expected.emplace_back("G", FileKind::Add());
+ ContainSameEntryFile(merged.value(), entry_file_expected);
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/core/operation/metrics/commit_metrics.h
b/src/paimon/core/operation/metrics/commit_metrics.h
new file mode 100644
index 0000000..cb26a12
--- /dev/null
+++ b/src/paimon/core/operation/metrics/commit_metrics.h
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+namespace paimon {
+
+/// Metrics to measure a commit.
+class CommitMetrics {
+ public:
+ static constexpr char LAST_COMMIT_DURATION[] = "lastCommitDuration";
+ static constexpr char LAST_COMMIT_ATTEMPTS[] = "lastCommitAttempts";
+ static constexpr char LAST_TABLE_FILES_ADDED[] = "lastTableFilesAdded";
+ static constexpr char LAST_TABLE_FILES_DELETED[] = "lastTableFilesDeleted";
+ static constexpr char LAST_TABLE_FILES_APPENDED[] =
"lastTableFilesAppended";
+ static constexpr char LAST_TABLE_FILES_COMMIT_COMPACTED[] =
"lastTableFilesCommitCompacted";
+ static constexpr char LAST_CHANGELOG_FILES_APPENDED[] =
"lastChangelogFilesAppended";
+ static constexpr char LAST_CHANGELOG_FILES_COMMIT_COMPACTED[] =
+ "lastChangelogFileCommitCompacted";
+ static constexpr char LAST_GENERATED_SNAPSHOTS[] =
"lastGeneratedSnapshots";
+ static constexpr char LAST_DELTA_RECORDS_APPENDED[] =
"lastDeltaRecordsAppended";
+ static constexpr char LAST_CHANGELOG_RECORDS_APPENDED[] =
"lastChangelogRecordsAppended";
+ static constexpr char LAST_DELTA_RECORDS_COMMIT_COMPACTED[] =
"lastDeltaRecordsCommitCompacted";
+ static constexpr char LAST_CHANGELOG_RECORDS_COMMIT_COMPACTED[] =
+ "lastChangelogRecordsCommitCompacted";
+ static constexpr char LAST_PARTITIONS_WRITTEN[] = "lastPartitionsWritten";
+ static constexpr char LAST_BUCKETS_WRITTEN[] = "lastBucketsWritten";
+ static constexpr char LAST_COMPACTION_INPUT_FILE_SIZE[] =
"lastCompactionInputFileSize";
+ static constexpr char LAST_COMPACTION_OUTPUT_FILE_SIZE[] =
"lastCompactionOutputFileSize";
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/metrics/commit_metrics_test.cpp
b/src/paimon/core/operation/metrics/commit_metrics_test.cpp
new file mode 100644
index 0000000..787a01d
--- /dev/null
+++ b/src/paimon/core/operation/metrics/commit_metrics_test.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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/metrics/commit_metrics.h"
+
+#include <memory>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(CommitMetricsTest, TestSimple) {
+ auto commit_metrics = std::make_shared<MetricsImpl>();
+ commit_metrics->SetCounter("some_metric", 100);
+ commit_metrics->SetCounter(CommitMetrics::LAST_COMMIT_ATTEMPTS, 30);
+ ASSERT_OK_AND_ASSIGN(uint64_t counter,
+
commit_metrics->GetCounter(CommitMetrics::LAST_COMMIT_ATTEMPTS));
+ ASSERT_EQ(30, counter);
+ ASSERT_OK_AND_ASSIGN(counter, commit_metrics->GetCounter("some_metric"));
+ ASSERT_EQ(100, counter);
+ auto other = std::make_shared<MetricsImpl>();
+ other->SetCounter("some_metric_2", 200);
+ other->SetCounter(CommitMetrics::LAST_COMMIT_ATTEMPTS, 50);
+ commit_metrics->Merge(other);
+ ASSERT_OK_AND_ASSIGN(counter,
commit_metrics->GetCounter(CommitMetrics::LAST_COMMIT_ATTEMPTS));
+ ASSERT_EQ(80, counter);
+ ASSERT_OK_AND_ASSIGN(counter, commit_metrics->GetCounter("some_metric"));
+ ASSERT_EQ(100, counter);
+ ASSERT_OK_AND_ASSIGN(counter, commit_metrics->GetCounter("some_metric_2"));
+ ASSERT_EQ(200, counter);
+}
+
+} // namespace paimon::test