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 d945409 feat: add snapshot, snapshot_info, and table core utilities
with tests (#118)
d945409 is described below
commit d945409532b8db12e113082c9ec51230bcf9bedb
Author: Yonghao Fang <[email protected]>
AuthorDate: Tue Jun 30 16:56:42 2026 +0800
feat: add snapshot, snapshot_info, and table core utilities with tests
(#118)
---
include/paimon/snapshot/snapshot_info.h | 53 +++++
src/paimon/core/snapshot.cpp | 329 ++++++++++++++++++++++++++
src/paimon/core/snapshot.h | 309 ++++++++++++++++++++++++
src/paimon/core/snapshot_info.cpp | 39 +++
src/paimon/core/snapshot_test.cpp | 407 ++++++++++++++++++++++++++++++++
src/paimon/core/table/bucket_mode.h | 67 ++++++
src/paimon/core/table/table.cpp | 54 +++++
src/paimon/core/table/table_test.cpp | 91 +++++++
8 files changed, 1349 insertions(+)
diff --git a/include/paimon/snapshot/snapshot_info.h
b/include/paimon/snapshot/snapshot_info.h
new file mode 100644
index 0000000..726b015
--- /dev/null
+++ b/include/paimon/snapshot/snapshot_info.h
@@ -0,0 +1,53 @@
+/*
+ * 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 <optional>
+#include <string>
+
+#include "paimon/visibility.h"
+
+namespace paimon {
+
+/// Plain snapshot metadata returned by Catalog::ListSnapshots().
+struct PAIMON_EXPORT SnapshotInfo {
+ /// Commit kind exposed through the public Catalog API.
+ enum class PAIMON_EXPORT CommitKind : int8_t {
+ APPEND,
+ COMPACT,
+ OVERWRITE,
+ ANALYZE,
+ UNKNOWN,
+ };
+
+ /// Convert a CommitKind to its canonical string representation.
+ static std::string CommitKindToString(CommitKind kind);
+
+ int64_t snapshot_id;
+ int64_t schema_id;
+ std::string commit_user;
+ CommitKind commit_kind;
+ int64_t time_millis;
+ std::optional<int64_t> total_record_count;
+ std::optional<int64_t> delta_record_count;
+ std::optional<int64_t> watermark;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/snapshot.cpp b/src/paimon/core/snapshot.cpp
new file mode 100644
index 0000000..5ba177e
--- /dev/null
+++ b/src/paimon/core/snapshot.cpp
@@ -0,0 +1,329 @@
+/*
+ * 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/snapshot.h"
+
+#include <cassert>
+#include <stdexcept>
+#include <utility>
+
+#include "paimon/common/utils/rapidjson_util.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "rapidjson/allocators.h"
+#include "rapidjson/document.h"
+#include "rapidjson/rapidjson.h"
+
+namespace paimon {
+
+const Snapshot::CommitKind Snapshot::CommitKind::Append() {
+ static const Snapshot::CommitKind commit_kind = Snapshot::CommitKind(0);
+ return commit_kind;
+}
+
+const Snapshot::CommitKind Snapshot::CommitKind::Compact() {
+ static const Snapshot::CommitKind commit_kind = Snapshot::CommitKind(1);
+ return commit_kind;
+}
+
+const Snapshot::CommitKind Snapshot::CommitKind::Overwrite() {
+ static const Snapshot::CommitKind commit_kind = Snapshot::CommitKind(2);
+ return commit_kind;
+}
+
+const Snapshot::CommitKind Snapshot::CommitKind::Analyze() {
+ static const Snapshot::CommitKind commit_kind = Snapshot::CommitKind(3);
+ return commit_kind;
+}
+
+const Snapshot::CommitKind Snapshot::CommitKind::Unknown() {
+ static const Snapshot::CommitKind commit_kind = Snapshot::CommitKind(-1);
+ return commit_kind;
+}
+bool Snapshot::TEST_Equal(const Snapshot& other) const {
+ if (this == &other) {
+ return true;
+ }
+
+ if ((base_manifest_list_size_ && !other.base_manifest_list_size_) ||
+ (!base_manifest_list_size_ && other.base_manifest_list_size_)) {
+ return false;
+ }
+ if ((delta_manifest_list_size_ && !other.delta_manifest_list_size_) ||
+ (!delta_manifest_list_size_ && other.delta_manifest_list_size_)) {
+ return false;
+ }
+ if ((changelog_manifest_list_ && !other.changelog_manifest_list_) ||
+ (!changelog_manifest_list_ && other.changelog_manifest_list_)) {
+ return false;
+ }
+ if ((changelog_manifest_list_size_ &&
!other.changelog_manifest_list_size_) ||
+ (!changelog_manifest_list_size_ &&
other.changelog_manifest_list_size_)) {
+ return false;
+ }
+
+ return version_ == other.version_ && id_ == other.id_ && schema_id_ ==
other.schema_id_ &&
+ index_manifest_ == other.index_manifest_ && commit_user_ ==
other.commit_user_ &&
+ commit_identifier_ == other.commit_identifier_ && commit_kind_ ==
other.commit_kind_ &&
+ log_offsets_ == other.log_offsets_ && total_record_count_ ==
other.total_record_count_ &&
+ delta_record_count_ == other.delta_record_count_ &&
+ changelog_record_count_ == other.changelog_record_count_ &&
+ watermark_ == other.watermark_ && statistics_ == other.statistics_
&&
+ properties_ == other.properties_ && next_row_id_ ==
other.next_row_id_;
+}
+
+bool Snapshot::operator==(const Snapshot& other) const {
+ if (this == &other) {
+ return true;
+ }
+ return version_ == other.version_ && id_ == other.id_ && schema_id_ ==
other.schema_id_ &&
+ base_manifest_list_ == other.base_manifest_list_ &&
+ base_manifest_list_size_ == other.base_manifest_list_size_ &&
+ delta_manifest_list_ == other.delta_manifest_list_ &&
+ delta_manifest_list_size_ == other.delta_manifest_list_size_ &&
+ changelog_manifest_list_ == other.changelog_manifest_list_ &&
+ changelog_manifest_list_size_ ==
other.changelog_manifest_list_size_ &&
+ index_manifest_ == other.index_manifest_ && commit_user_ ==
other.commit_user_ &&
+ commit_identifier_ == other.commit_identifier_ && commit_kind_ ==
other.commit_kind_ &&
+ time_millis_ == other.time_millis_ && log_offsets_ ==
other.log_offsets_ &&
+ total_record_count_ == other.total_record_count_ &&
+ delta_record_count_ == other.delta_record_count_ &&
+ changelog_record_count_ == other.changelog_record_count_ &&
+ watermark_ == other.watermark_ && statistics_ == other.statistics_
&&
+ properties_ == other.properties_ && next_row_id_ ==
other.next_row_id_;
+}
+
+std::string Snapshot::CommitKind::ToString(const Snapshot::CommitKind& kind) {
+ switch (kind.value_) {
+ case 0:
+ return "APPEND";
+ case 1:
+ return "COMPACT";
+ case 2:
+ return "OVERWRITE";
+ case 3:
+ return "ANALYZE";
+ default:
+ assert(false);
+ return "UNKNOWN";
+ }
+}
+Snapshot::CommitKind Snapshot::CommitKind::FromString(const std::string& kind)
{
+ if (kind == "APPEND") {
+ return Append();
+ } else if (kind == "COMPACT") {
+ return Compact();
+ } else if (kind == "OVERWRITE") {
+ return Overwrite();
+ } else if (kind == "ANALYZE") {
+ return Analyze();
+ }
+ assert(false);
+ return Unknown();
+}
+
+Snapshot::Snapshot(const std::optional<int32_t>& version, int64_t id, int64_t
schema_id,
+ const std::string& base_manifest_list,
+ const std::optional<int64_t>& base_manifest_list_size,
+ const std::string& delta_manifest_list,
+ const std::optional<int64_t>& delta_manifest_list_size,
+ const std::optional<std::string>& changelog_manifest_list,
+ const std::optional<int64_t>& changelog_manifest_list_size,
+ const std::optional<std::string>& index_manifest, const
std::string& commit_user,
+ int64_t commit_identifier, CommitKind commit_kind, int64_t
time_millis,
+ const std::optional<std::map<int32_t, int64_t>>&
log_offsets,
+ const std::optional<int64_t>& total_record_count,
+ const std::optional<int64_t>& delta_record_count,
+ const std::optional<int64_t>& changelog_record_count,
+ const std::optional<int64_t>& watermark,
+ const std::optional<std::string>& statistics,
+ const std::optional<std::map<std::string, std::string>>&
properties,
+ const std::optional<int64_t>& next_row_id)
+ : version_(version),
+ id_(id),
+ schema_id_(schema_id),
+ base_manifest_list_(base_manifest_list),
+ base_manifest_list_size_(base_manifest_list_size),
+ delta_manifest_list_(delta_manifest_list),
+ delta_manifest_list_size_(delta_manifest_list_size),
+ changelog_manifest_list_(changelog_manifest_list),
+ changelog_manifest_list_size_(changelog_manifest_list_size),
+ index_manifest_(index_manifest),
+ commit_user_(commit_user),
+ commit_identifier_(commit_identifier),
+ commit_kind_(commit_kind),
+ time_millis_(time_millis),
+ log_offsets_(log_offsets),
+ total_record_count_(total_record_count),
+ delta_record_count_(delta_record_count),
+ changelog_record_count_(changelog_record_count),
+ watermark_(watermark),
+ statistics_(statistics),
+ properties_(properties),
+ next_row_id_(next_row_id) {}
+
+rapidjson::Value Snapshot::ToJson(rapidjson::Document::AllocatorType*
allocator) const
+ noexcept(false) {
+ rapidjson::Value obj(rapidjson::kObjectType);
+ obj.AddMember(rapidjson::StringRef(FIELD_VERSION),
+ RapidJsonUtil::SerializeValue(Version(), allocator).Move(),
*allocator);
+ obj.AddMember(rapidjson::StringRef(FIELD_ID),
+ RapidJsonUtil::SerializeValue(id_, allocator).Move(),
*allocator);
+ obj.AddMember(rapidjson::StringRef(FIELD_SCHEMA_ID),
+ RapidJsonUtil::SerializeValue(schema_id_, allocator).Move(),
*allocator);
+ obj.AddMember(rapidjson::StringRef(FIELD_BASE_MANIFEST_LIST),
+ RapidJsonUtil::SerializeValue(base_manifest_list_,
allocator).Move(), *allocator);
+ if (base_manifest_list_size_) {
+ obj.AddMember(
+ rapidjson::StringRef(FIELD_BASE_MANIFEST_LIST_SIZE),
+ RapidJsonUtil::SerializeValue(base_manifest_list_size_.value(),
allocator).Move(),
+ *allocator);
+ }
+ obj.AddMember(rapidjson::StringRef(FIELD_DELTA_MANIFEST_LIST),
+ RapidJsonUtil::SerializeValue(delta_manifest_list_,
allocator).Move(),
+ *allocator);
+ if (delta_manifest_list_size_) {
+ obj.AddMember(rapidjson::StringRef(FIELD_DELTA_MANIFEST_LIST_SIZE),
+ RapidJsonUtil::SerializeValue(delta_manifest_list_size_,
allocator).Move(),
+ *allocator);
+ }
+ obj.AddMember(rapidjson::StringRef(FIELD_CHANGELOG_MANIFEST_LIST),
+ RapidJsonUtil::SerializeValue(changelog_manifest_list_,
allocator).Move(),
+ *allocator);
+ if (changelog_manifest_list_size_) {
+ obj.AddMember(
+ rapidjson::StringRef(FIELD_CHANGELOG_MANIFEST_LIST_SIZE),
+ RapidJsonUtil::SerializeValue(changelog_manifest_list_size_,
allocator).Move(),
+ *allocator);
+ }
+ if (index_manifest_ != std::nullopt) {
+ obj.AddMember(rapidjson::StringRef(FIELD_INDEX_MANIFEST),
+ RapidJsonUtil::SerializeValue(index_manifest_.value(),
allocator).Move(),
+ *allocator);
+ }
+
+ obj.AddMember(rapidjson::StringRef(FIELD_COMMIT_USER),
+ RapidJsonUtil::SerializeValue(commit_user_,
allocator).Move(), *allocator);
+ obj.AddMember(rapidjson::StringRef(FIELD_COMMIT_IDENTIFIER),
+ RapidJsonUtil::SerializeValue(commit_identifier_,
allocator).Move(), *allocator);
+ obj.AddMember(
+ rapidjson::StringRef(FIELD_COMMIT_KIND),
+
RapidJsonUtil::SerializeValue(Snapshot::CommitKind::ToString(commit_kind_),
allocator)
+ .Move(),
+ *allocator);
+
+ obj.AddMember(rapidjson::StringRef(FIELD_TIME_MILLIS),
+ RapidJsonUtil::SerializeValue(time_millis_,
allocator).Move(), *allocator);
+ if (log_offsets_ != std::nullopt) {
+ obj.AddMember(rapidjson::StringRef(FIELD_LOG_OFFSETS),
+ RapidJsonUtil::SerializeValue(log_offsets_.value(),
allocator).Move(),
+ *allocator);
+ }
+ obj.AddMember(rapidjson::StringRef(FIELD_TOTAL_RECORD_COUNT),
+ RapidJsonUtil::SerializeValue(total_record_count_.value(),
allocator).Move(),
+ *allocator);
+ obj.AddMember(rapidjson::StringRef(FIELD_DELTA_RECORD_COUNT),
+ RapidJsonUtil::SerializeValue(delta_record_count_.value(),
allocator).Move(),
+ *allocator);
+
+ if (changelog_record_count_ != std::nullopt) {
+ obj.AddMember(
+ rapidjson::StringRef(FIELD_CHANGELOG_RECORD_COUNT),
+ RapidJsonUtil::SerializeValue(changelog_record_count_.value(),
allocator).Move(),
+ *allocator);
+ }
+
+ if (watermark_ != std::nullopt) {
+ obj.AddMember(rapidjson::StringRef(FIELD_WATERMARK),
+ RapidJsonUtil::SerializeValue(watermark_.value(),
allocator).Move(),
+ *allocator);
+ }
+
+ if (statistics_ != std::nullopt) {
+ obj.AddMember(rapidjson::StringRef(FIELD_STATISTICS),
+ RapidJsonUtil::SerializeValue(statistics_.value(),
allocator).Move(),
+ *allocator);
+ }
+ if (properties_ != std::nullopt) {
+ obj.AddMember(rapidjson::StringRef(FIELD_PROPERTIES),
+ RapidJsonUtil::SerializeValue(properties_.value(),
allocator).Move(),
+ *allocator);
+ }
+ if (next_row_id_ != std::nullopt) {
+ obj.AddMember(rapidjson::StringRef(FIELD_NEXT_ROW_ID),
+ RapidJsonUtil::SerializeValue(next_row_id_.value(),
allocator).Move(),
+ *allocator);
+ }
+
+ return obj;
+}
+
+void Snapshot::FromJson(const rapidjson::Value& obj) noexcept(false) {
+ version_ = RapidJsonUtil::DeserializeKeyValue<int32_t>(obj, FIELD_VERSION,
-1);
+ id_ = RapidJsonUtil::DeserializeKeyValue<int64_t>(obj, FIELD_ID);
+ schema_id_ = RapidJsonUtil::DeserializeKeyValue<int64_t>(obj,
FIELD_SCHEMA_ID);
+ base_manifest_list_ =
+ RapidJsonUtil::DeserializeKeyValue<std::string>(obj,
FIELD_BASE_MANIFEST_LIST);
+ base_manifest_list_size_ =
RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(
+ obj, FIELD_BASE_MANIFEST_LIST_SIZE);
+ delta_manifest_list_ =
+ RapidJsonUtil::DeserializeKeyValue<std::string>(obj,
FIELD_DELTA_MANIFEST_LIST);
+ delta_manifest_list_size_ =
RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(
+ obj, FIELD_DELTA_MANIFEST_LIST_SIZE);
+ changelog_manifest_list_ =
RapidJsonUtil::DeserializeKeyValue<std::optional<std::string>>(
+ obj, FIELD_CHANGELOG_MANIFEST_LIST);
+ changelog_manifest_list_size_ =
RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(
+ obj, FIELD_CHANGELOG_MANIFEST_LIST_SIZE);
+ index_manifest_ =
+ RapidJsonUtil::DeserializeKeyValue<std::optional<std::string>>(obj,
FIELD_INDEX_MANIFEST);
+ commit_user_ = RapidJsonUtil::DeserializeKeyValue<std::string>(obj,
FIELD_COMMIT_USER);
+ commit_identifier_ = RapidJsonUtil::DeserializeKeyValue<int64_t>(obj,
FIELD_COMMIT_IDENTIFIER);
+ commit_kind_ = Snapshot::CommitKind::FromString(
+ RapidJsonUtil::DeserializeKeyValue<std::string>(obj,
FIELD_COMMIT_KIND));
+ if (commit_kind_ == Snapshot::CommitKind::Unknown()) {
+ throw std::invalid_argument("deserialize CommitKind failed");
+ }
+ time_millis_ = RapidJsonUtil::DeserializeKeyValue<int64_t>(obj,
FIELD_TIME_MILLIS);
+ log_offsets_ =
RapidJsonUtil::DeserializeKeyValue<std::optional<std::map<int32_t, int64_t>>>(
+ obj, FIELD_LOG_OFFSETS);
+ total_record_count_ =
+ RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(obj,
FIELD_TOTAL_RECORD_COUNT);
+ delta_record_count_ =
+ RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(obj,
FIELD_DELTA_RECORD_COUNT);
+ changelog_record_count_ =
RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(
+ obj, FIELD_CHANGELOG_RECORD_COUNT);
+ watermark_ =
RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(obj,
FIELD_WATERMARK);
+ statistics_ =
+ RapidJsonUtil::DeserializeKeyValue<std::optional<std::string>>(obj,
FIELD_STATISTICS);
+ properties_ =
+ RapidJsonUtil::DeserializeKeyValue<std::optional<std::map<std::string,
std::string>>>(
+ obj, FIELD_PROPERTIES);
+ next_row_id_ =
+ RapidJsonUtil::DeserializeKeyValue<std::optional<int64_t>>(obj,
FIELD_NEXT_ROW_ID);
+}
+
+Result<Snapshot> Snapshot::FromPath(const std::shared_ptr<FileSystem>& fs,
+ const std::string& path) {
+ std::string json_str;
+ PAIMON_RETURN_NOT_OK(fs->ReadFile(path, &json_str));
+ PAIMON_ASSIGN_OR_RAISE(Snapshot snapshot,
Snapshot::FromJsonString(json_str));
+ return snapshot;
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/snapshot.h b/src/paimon/core/snapshot.h
new file mode 100644
index 0000000..827d416
--- /dev/null
+++ b/src/paimon/core/snapshot.h
@@ -0,0 +1,309 @@
+/*
+ * 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 <limits>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "paimon/common/utils/jsonizable.h"
+#include "paimon/result.h"
+#include "paimon/type_fwd.h"
+#include "rapidjson/allocators.h"
+#include "rapidjson/document.h"
+#include "rapidjson/rapidjson.h"
+
+namespace paimon {
+class FileSystem;
+
+// This file is the entrance to all data committed at some specific time point.
+class Snapshot : public Jsonizable<Snapshot> {
+ public:
+ class CommitKind {
+ public:
+ explicit CommitKind(int8_t kind) : value_(kind) {}
+ /// Changes flushed from the mem table.
+ static const CommitKind Append();
+
+ /// Changes by compacting existing data files.
+ static const CommitKind Compact();
+
+ /// Changes that clear up the whole partition and then add new records.
+ static const CommitKind Overwrite();
+
+ /// Collect statistics.
+ static const CommitKind Analyze();
+
+ static const CommitKind Unknown();
+
+ bool operator==(const CommitKind& other) const {
+ return value_ == other.value_;
+ }
+ static std::string ToString(const CommitKind& kind);
+ static CommitKind FromString(const std::string& kind);
+
+ private:
+ int8_t value_;
+ };
+
+ static constexpr char FIELD_VERSION[] = "version";
+ static constexpr char FIELD_ID[] = "id";
+ static constexpr char FIELD_SCHEMA_ID[] = "schemaId";
+ static constexpr char FIELD_BASE_MANIFEST_LIST[] = "baseManifestList";
+ static constexpr char FIELD_BASE_MANIFEST_LIST_SIZE[] =
"baseManifestListSize";
+ static constexpr char FIELD_DELTA_MANIFEST_LIST[] = "deltaManifestList";
+ static constexpr char FIELD_DELTA_MANIFEST_LIST_SIZE[] =
"deltaManifestListSize";
+ static constexpr char FIELD_CHANGELOG_MANIFEST_LIST[] =
"changelogManifestList";
+ static constexpr char FIELD_CHANGELOG_MANIFEST_LIST_SIZE[] =
"changelogManifestListSize";
+ static constexpr char FIELD_INDEX_MANIFEST[] = "indexManifest";
+ static constexpr char FIELD_COMMIT_USER[] = "commitUser";
+ static constexpr char FIELD_COMMIT_IDENTIFIER[] = "commitIdentifier";
+ static constexpr char FIELD_COMMIT_KIND[] = "commitKind";
+ static constexpr char FIELD_TIME_MILLIS[] = "timeMillis";
+ static constexpr char FIELD_LOG_OFFSETS[] = "logOffsets";
+ static constexpr char FIELD_TOTAL_RECORD_COUNT[] = "totalRecordCount";
+ static constexpr char FIELD_DELTA_RECORD_COUNT[] = "deltaRecordCount";
+ static constexpr char FIELD_CHANGELOG_RECORD_COUNT[] =
"changelogRecordCount";
+ static constexpr char FIELD_WATERMARK[] = "watermark";
+ static constexpr char FIELD_STATISTICS[] = "statistics";
+ static constexpr char FIELD_PROPERTIES[] = "properties";
+ static constexpr char FIELD_NEXT_ROW_ID[] = "nextRowId";
+
+ JSONIZABLE_FRIEND_AND_DEFAULT_CTOR(Snapshot);
+
+ Snapshot(int64_t id, int64_t schema_id, const std::string&
base_manifest_list,
+ const std::optional<int64_t>& base_manifest_list_size,
+ const std::string& delta_manifest_list,
+ const std::optional<int64_t>& delta_manifest_list_size,
+ const std::optional<std::string>& changelog_manifest_list,
+ const std::optional<int64_t>& changelog_manifest_list_size,
+ const std::optional<std::string>& index_manifest, const
std::string& commit_user,
+ int64_t commit_identifier, CommitKind commit_kind, int64_t
time_millis,
+ const std::optional<std::map<int32_t, int64_t>>& log_offsets,
+ const std::optional<int64_t>& total_record_count,
+ const std::optional<int64_t>& delta_record_count,
+ const std::optional<int64_t>& changelog_record_count,
+ const std::optional<int64_t>& watermark, const
std::optional<std::string>& statistics,
+ const std::optional<std::map<std::string, std::string>>&
properties,
+ const std::optional<int64_t>& next_row_id)
+ : Snapshot(CURRENT_VERSION, id, schema_id, base_manifest_list,
base_manifest_list_size,
+ delta_manifest_list, delta_manifest_list_size,
changelog_manifest_list,
+ changelog_manifest_list_size, index_manifest, commit_user,
commit_identifier,
+ commit_kind, time_millis, log_offsets, total_record_count,
delta_record_count,
+ changelog_record_count, watermark, statistics, properties,
next_row_id) {}
+
+ Snapshot(const std::optional<int32_t>& version, int64_t id, int64_t
schema_id,
+ const std::string& base_manifest_list,
+ const std::optional<int64_t>& base_manifest_list_size,
+ const std::string& delta_manifest_list,
+ const std::optional<int64_t>& delta_manifest_list_size,
+ const std::optional<std::string>& changelog_manifest_list,
+ const std::optional<int64_t>& changelog_manifest_list_size,
+ const std::optional<std::string>& index_manifest, const
std::string& commit_user,
+ int64_t commit_identifier, CommitKind commit_kind, int64_t
time_millis,
+ const std::optional<std::map<int32_t, int64_t>>& log_offsets,
+ const std::optional<int64_t>& total_record_count,
+ const std::optional<int64_t>& delta_record_count,
+ const std::optional<int64_t>& changelog_record_count,
+ const std::optional<int64_t>& watermark, const
std::optional<std::string>& statistics,
+ const std::optional<std::map<std::string, std::string>>&
properties,
+ const std::optional<int64_t>& next_row_id);
+
+ bool operator==(const Snapshot& other) const;
+ bool TEST_Equal(const Snapshot& other) const;
+
+ public:
+ static constexpr int64_t FIRST_SNAPSHOT_ID = 1;
+ static constexpr int32_t TABLE_STORE_02_VERSION = 1;
+ static constexpr int32_t CURRENT_VERSION = 3;
+
+ public:
+ int32_t Version() const {
+ // there is no version field for paimon <= 0.2
+ return version_ == std::nullopt ? TABLE_STORE_02_VERSION :
version_.value();
+ }
+
+ int64_t Id() const {
+ return id_;
+ }
+
+ int64_t SchemaId() const {
+ return schema_id_;
+ }
+
+ const std::string& BaseManifestList() const {
+ return base_manifest_list_;
+ }
+
+ const std::optional<int64_t>& BaseManifestListSize() const {
+ return base_manifest_list_size_;
+ }
+
+ const std::string& DeltaManifestList() const {
+ return delta_manifest_list_;
+ }
+
+ const std::optional<int64_t>& DeltaManifestListSize() const {
+ return delta_manifest_list_size_;
+ }
+
+ const std::optional<std::string>& ChangelogManifestList() const {
+ return changelog_manifest_list_;
+ }
+
+ const std::optional<int64_t>& ChangelogManifestListSize() const {
+ return changelog_manifest_list_size_;
+ }
+
+ const std::optional<std::string>& IndexManifest() const {
+ return index_manifest_;
+ }
+
+ const std::string& CommitUser() const {
+ return commit_user_;
+ }
+
+ int64_t CommitIdentifier() const {
+ return commit_identifier_;
+ }
+
+ CommitKind GetCommitKind() const {
+ return commit_kind_;
+ }
+
+ int64_t TimeMillis() const {
+ return time_millis_;
+ }
+
+ const std::optional<std::map<int32_t, int64_t>>& LogOffsets() const {
+ return log_offsets_;
+ }
+
+ const std::optional<int64_t>& TotalRecordCount() const {
+ return total_record_count_;
+ }
+
+ const std::optional<int64_t>& DeltaRecordCount() const {
+ return delta_record_count_;
+ }
+
+ const std::optional<int64_t>& ChangelogRecordCount() const {
+ return changelog_record_count_;
+ }
+
+ const std::optional<int64_t>& Watermark() const {
+ return watermark_;
+ }
+
+ const std::optional<std::string>& Statistics() const {
+ return statistics_;
+ }
+
+ const std::optional<std::map<std::string, std::string>>& Properties()
const {
+ return properties_;
+ }
+
+ const std::optional<int64_t>& NextRowId() const {
+ return next_row_id_;
+ }
+
+ rapidjson::Value ToJson(rapidjson::Document::AllocatorType* allocator)
const
+ noexcept(false) override;
+
+ void FromJson(const rapidjson::Value& obj) noexcept(false) override;
+
+ static Result<Snapshot> FromPath(const std::shared_ptr<FileSystem>& fs,
+ const std::string& path);
+
+ private:
+ // version of snapshot
+ // null for paimon <= 0.2
+ std::optional<int32_t> version_;
+ int64_t id_ = -1;
+ int64_t schema_id_ = -1;
+
+ // a manifest list recording all changes from the previous snapshots
+ std::string base_manifest_list_;
+ std::optional<int64_t> base_manifest_list_size_;
+
+ // a manifest list recording all new changes occurred in this snapshot
+ // for faster expire and streaming reads
+ std::string delta_manifest_list_;
+ std::optional<int64_t> delta_manifest_list_size_;
+
+ // a manifest list recording all changelog produced in this snapshot
+ // null if no changelog is produced, or for paimon <= 0.2
+ std::optional<std::string> changelog_manifest_list_;
+ std::optional<int64_t> changelog_manifest_list_size_;
+
+ // a manifest recording all index files of this table
+ // null if no index file
+ std::optional<std::string> index_manifest_;
+
+ std::string commit_user_;
+
+ // Mainly for snapshot deduplication.
+ //
+ // If multiple snapshots have the same commitIdentifier, reading from any
of these
+ // snapshots must produce the same table.
+ //
+ // If snapshot A has a smaller commitIdentifier than snapshot B, then
snapshot A must
+ // be committed before snapshot B, and thus snapshot A must contain older
records than
+ // snapshot B.
+ int64_t commit_identifier_ = std::numeric_limits<int64_t>::min();
+
+ CommitKind commit_kind_ = CommitKind::Unknown();
+
+ int64_t time_millis_;
+
+ std::optional<std::map<int32_t, int64_t>> log_offsets_;
+
+ // record count of all changes occurred in this snapshot
+ // null for paimon <= 0.3
+ std::optional<int64_t> total_record_count_;
+
+ // record count of all new changes occurred in this snapshot
+ // null for paimon <= 0.3
+ std::optional<int64_t> delta_record_count_;
+
+ // record count of all changelog produced in this snapshot
+ // null for paimon <= 0.3
+ std::optional<int64_t> changelog_record_count_;
+
+ // watermark for input records
+ // null for paimon <= 0.3
+ // null if there is no watermark in new committing, and the previous
snapshot does not
+ // Have a watermark
+ std::optional<int64_t> watermark_;
+
+ // stats file name for statistics of this table
+ // null if no stats file
+ std::optional<std::string> statistics_;
+
+ // properties
+ // null for paimon <= 1.1 or empty properties
+ std::optional<std::map<std::string, std::string>> properties_;
+
+ std::optional<int64_t> next_row_id_;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/snapshot_info.cpp
b/src/paimon/core/snapshot_info.cpp
new file mode 100644
index 0000000..46b6467
--- /dev/null
+++ b/src/paimon/core/snapshot_info.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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/snapshot/snapshot_info.h"
+
+namespace paimon {
+
+std::string SnapshotInfo::CommitKindToString(CommitKind kind) {
+ switch (kind) {
+ case CommitKind::APPEND:
+ return "APPEND";
+ case CommitKind::COMPACT:
+ return "COMPACT";
+ case CommitKind::OVERWRITE:
+ return "OVERWRITE";
+ case CommitKind::ANALYZE:
+ return "ANALYZE";
+ case CommitKind::UNKNOWN:
+ return "UNKNOWN";
+ }
+ return "UNKNOWN";
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/snapshot_test.cpp
b/src/paimon/core/snapshot_test.cpp
new file mode 100644
index 0000000..b5694e4
--- /dev/null
+++ b/src/paimon/core/snapshot_test.cpp
@@ -0,0 +1,407 @@
+/*
+ * 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/snapshot.h"
+
+#include "gtest/gtest.h"
+#include "paimon/common/utils/string_utils.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/result.h"
+#include "paimon/snapshot/snapshot_info.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+class SnapshotTest : public testing::Test {
+ public:
+ std::string ReplaceAll(const std::string& str) {
+ std::string replaced_str = StringUtils::Replace(str, " ", "");
+ replaced_str = StringUtils::Replace(replaced_str, "\t", "");
+ replaced_str = StringUtils::Replace(replaced_str, "\n", "");
+ return replaced_str;
+ }
+};
+
+TEST_F(SnapshotTest, TestSimple) {
+ std::map<int32_t, int64_t> log_offset = {{25, 30}};
+ std::map<std::string, std::string> properties = {{"key1", "value1"},
{"key2", "value2"}};
+ Snapshot snapshot(
+ /*version=*/5, /*id=*/10, /*schema_id=*/15,
/*base_manifest_list=*/"base_manifest_list", 10,
+ /*delta_manifest_list=*/"delta_manifest_list", 20,
+ /*changelog_manifest_list=*/"changelog_manifest_list", 30,
+ /*index_manifest=*/"index_manifest",
+ /*commit_user=*/"commit_user_01", /*commit_identifier=*/20,
+ /*commit_kind=*/Snapshot::CommitKind::Compact(), /*time_millis=*/1234,
log_offset,
+ /*total_record_count=*/35,
+ /*delta_record_count=*/40, /*changelog_record_count=*/45,
/*watermark=*/50,
+ /*statistics=*/"statistic_test", properties, /*next_row_id=*/0);
+ ASSERT_EQ(5, snapshot.Version());
+ ASSERT_EQ(10, snapshot.Id());
+ ASSERT_EQ(15, snapshot.SchemaId());
+ ASSERT_EQ("base_manifest_list", snapshot.BaseManifestList());
+ ASSERT_EQ(10, snapshot.BaseManifestListSize().value());
+ ASSERT_EQ("delta_manifest_list", snapshot.DeltaManifestList());
+ ASSERT_EQ(20, snapshot.DeltaManifestListSize().value());
+ ASSERT_EQ("changelog_manifest_list",
snapshot.ChangelogManifestList().value());
+ ASSERT_EQ(30, snapshot.ChangelogManifestListSize().value());
+ ASSERT_EQ("index_manifest", snapshot.IndexManifest().value());
+ ASSERT_EQ("commit_user_01", snapshot.CommitUser());
+ ASSERT_EQ(20, snapshot.CommitIdentifier());
+ ASSERT_EQ(Snapshot::CommitKind::Compact(), snapshot.GetCommitKind());
+ ASSERT_EQ(1234, snapshot.TimeMillis());
+ ASSERT_EQ(log_offset, snapshot.LogOffsets().value());
+ ASSERT_EQ(35, snapshot.TotalRecordCount().value());
+ ASSERT_EQ(40, snapshot.DeltaRecordCount().value());
+ ASSERT_EQ(45, snapshot.ChangelogRecordCount().value());
+ ASSERT_EQ(50, snapshot.Watermark().value());
+ ASSERT_EQ("statistic_test", snapshot.Statistics().value());
+ ASSERT_EQ(properties, snapshot.Properties().value());
+ ASSERT_EQ(0, snapshot.NextRowId().value());
+}
+
+TEST_F(SnapshotTest, TestFromPath) {
+ std::string data_path =
+ paimon::test::GetDataDir() +
"/orc/append_09.db/append_09/snapshot/snapshot-1";
+ auto fs = std::make_shared<LocalFileSystem>();
+ ASSERT_OK_AND_ASSIGN(Snapshot snapshot, Snapshot::FromPath(fs, data_path));
+ ASSERT_EQ(3, snapshot.Version());
+ ASSERT_EQ(1, snapshot.Id());
+ ASSERT_EQ(0, snapshot.SchemaId());
+ ASSERT_EQ("manifest-list-616d1847-a02c-495f-9cca-2c8b7def0fec-0",
snapshot.BaseManifestList());
+ ASSERT_EQ(std::nullopt, snapshot.BaseManifestListSize());
+ ASSERT_EQ("manifest-list-616d1847-a02c-495f-9cca-2c8b7def0fec-1",
snapshot.DeltaManifestList());
+ ASSERT_EQ(std::nullopt, snapshot.DeltaManifestListSize());
+ ASSERT_EQ(std::nullopt, snapshot.ChangelogManifestList());
+ ASSERT_EQ(std::nullopt, snapshot.ChangelogManifestListSize());
+ ASSERT_EQ(std::nullopt, snapshot.IndexManifest());
+ ASSERT_EQ("b02e4322-9c5f-41e1-a560-c0156fdf7b9c", snapshot.CommitUser());
+ ASSERT_EQ(9223372036854775807ll, snapshot.CommitIdentifier());
+ ASSERT_EQ(Snapshot::CommitKind::Append(), snapshot.GetCommitKind());
+ ASSERT_EQ(1721614343270ll, snapshot.TimeMillis());
+ ASSERT_EQ((std::map<int32_t, int64_t>()), snapshot.LogOffsets().value());
+ ASSERT_EQ(5, snapshot.TotalRecordCount().value());
+ ASSERT_EQ(5, snapshot.DeltaRecordCount().value());
+ ASSERT_EQ(0, snapshot.ChangelogRecordCount().value());
+ ASSERT_EQ(std::nullopt, snapshot.Watermark());
+ ASSERT_EQ(std::nullopt, snapshot.Statistics());
+ ASSERT_EQ(std::nullopt, snapshot.Properties());
+ ASSERT_EQ(std::nullopt, snapshot.NextRowId());
+}
+
+TEST_F(SnapshotTest, TestJsonizable) {
+ std::string json_str = R"({
+ "version" : 3,
+ "id" : 1,
+ "schemaId" : 0,
+ "baseManifestList" :
"manifest-list-d96fcc30-99e8-4f45-962b-a1157c56f378-0",
+ "baseManifestListSize" : 20,
+ "deltaManifestList" :
"manifest-list-d96fcc30-99e8-4f45-962b-a1157c56f378-1",
+ "deltaManifestListSize" : 50,
+ "changelogManifestList" : null,
+ "commitUser" : "0e4d92f7-53b0-40d6-a7c0-102bf3801e6a",
+ "commitIdentifier" : 9223372036854775807,
+ "commitKind" : "OVERWRITE",
+ "timeMillis" : 1711692199281,
+ "logOffsets" : { },
+ "totalRecordCount" : 3,
+ "deltaRecordCount" : 3,
+ "changelogRecordCount" : 0
+ })";
+
+ ASSERT_OK_AND_ASSIGN(Snapshot snapshot,
Snapshot::FromJsonString(json_str));
+
+ Snapshot expected_snapshot(
+ /*version=*/3, /*id=*/1, /*schema_id=*/0, /*base_manifest_list=*/
+ "manifest-list-d96fcc30-99e8-4f45-962b-a1157c56f378-0",
/*base_manifest_list_size=*/20,
+
/*delta_manifest_list=*/"manifest-list-d96fcc30-99e8-4f45-962b-a1157c56f378-1",
+ /*delta_manifest_list_size=*/50,
/*changelog_manifest_list=*/std::nullopt,
+ /*changelog_manifest_list_size=*/std::nullopt,
/*index_manifest=*/std::nullopt,
+ /*commit_user=*/"0e4d92f7-53b0-40d6-a7c0-102bf3801e6a",
+ /*commit_identifier=*/9223372036854775807ll,
+ /*commit_kind=*/Snapshot::CommitKind::Overwrite(),
/*time_millis=*/1711692199281ll,
+ /*log_offsets=*/std::map<int32_t, int64_t>(),
+ /*total_record_count=*/3, /*delta_record_count=*/3,
/*changelog_record_count=*/0,
+ /*watermark=*/std::nullopt, /*statistics=*/std::nullopt,
/*properties=*/std::nullopt,
+ /*next_row_id=*/std::nullopt);
+ ASSERT_EQ(expected_snapshot, snapshot);
+
+ ASSERT_OK_AND_ASSIGN(std::string new_json_str, snapshot.ToJsonString());
+ ASSERT_EQ(ReplaceAll(json_str), ReplaceAll(new_json_str));
+}
+
+TEST_F(SnapshotTest, TestSerializeAndDeserialize) {
+ auto se_and_de = [&](const std::string& data_path) {
+ auto fs = std::make_shared<LocalFileSystem>();
+ std::string json_str;
+ ASSERT_OK(fs->ReadFile(data_path, &json_str));
+ ASSERT_OK_AND_ASSIGN(Snapshot snapshot, Snapshot::FromPath(fs,
data_path));
+ ASSERT_EQ(snapshot, snapshot);
+ ASSERT_OK_AND_ASSIGN(std::string se_json_str, snapshot.ToJsonString());
+ ASSERT_EQ(ReplaceAll(json_str), ReplaceAll(se_json_str));
+ };
+ auto se_and_de_from_str = [&](const std::string& json_str) {
+ ASSERT_OK_AND_ASSIGN(Snapshot snapshot,
Snapshot::FromJsonString(json_str));
+ ASSERT_EQ(snapshot, snapshot);
+ ASSERT_OK_AND_ASSIGN(std::string se_json_str, snapshot.ToJsonString());
+ ASSERT_EQ(ReplaceAll(json_str), ReplaceAll(se_json_str));
+ };
+
+ {
+ // without indexManifest
+ std::string data_path =
+ paimon::test::GetDataDir() +
+
"/orc/pk_table_scan_and_read_dv.db/pk_table_scan_and_read_dv/snapshot/snapshot-1";
+ se_and_de(data_path);
+ }
+ {
+ // with indexManifest
+ std::string data_path =
+ paimon::test::GetDataDir() +
+
"/orc/pk_table_scan_and_read_dv.db/pk_table_scan_and_read_dv/snapshot/snapshot-6";
+ se_and_de(data_path);
+ }
+ {
+ // with ManifestListSize
+ std::string data_path = paimon::test::GetDataDir() +
+ "/orc/append_with_bsi_bitmap_bloomfilter.db/"
+
"append_with_bsi_bitmap_bloomfilter/snapshot/snapshot-1";
+ se_and_de(data_path);
+ }
+ {
+ // with properties
+ std::string json_str = R"({
+ "version" : 3,
+ "id" : 10,
+ "schemaId" : 2,
+ "baseManifestList" : "base-manifest-list-1",
+ "baseManifestListSize" : 100,
+ "deltaManifestList" : "delta-manifest-list-2",
+ "deltaManifestListSize" : 200,
+ "changelogManifestList" : null,
+ "commitUser" : "commit-usr-3",
+ "commitIdentifier" : 12,
+ "commitKind" : "APPEND",
+ "timeMillis" : 1749724197266,
+ "logOffsets" : {
+ "0" : 1,
+ "1" : 3
+ },
+ "totalRecordCount" : 1024,
+ "deltaRecordCount" : 4096,
+ "watermark" : 1749724196266,
+ "statistics" : "statistics-4",
+ "properties" : {
+ "key0" : "value0",
+ "key1" : "value1"
+ }
+ })";
+ se_and_de_from_str(json_str);
+ }
+ {
+ // with next_row_id
+ std::string json_str = R"({
+ "version" : 3,
+ "id" : 10,
+ "schemaId" : 2,
+ "baseManifestList" : "base-manifest-list-1",
+ "baseManifestListSize" : 100,
+ "deltaManifestList" : "delta-manifest-list-2",
+ "deltaManifestListSize" : 200,
+ "changelogManifestList" : null,
+ "commitUser" : "commit-usr-3",
+ "commitIdentifier" : 12,
+ "commitKind" : "APPEND",
+ "timeMillis" : 1749724197266,
+ "logOffsets" : {
+ "0" : 1,
+ "1" : 3
+ },
+ "totalRecordCount" : 1024,
+ "deltaRecordCount" : 4096,
+ "watermark" : 1749724196266,
+ "statistics" : "statistics-4",
+ "properties" : {
+ "key0" : "value0",
+ "key1" : "value1"
+ },
+ "nextRowId" : 0
+ })";
+ se_and_de_from_str(json_str);
+ }
+}
+
+TEST_F(SnapshotTest, TestCommitKindAnalyze) {
+ // Test constructing a Snapshot with CommitKind::Analyze
+ Snapshot snapshot(
+ /*version=*/3, /*id=*/20, /*schema_id=*/5,
+ /*base_manifest_list=*/"base-manifest-analyze",
+ /*base_manifest_list_size=*/100,
+ /*delta_manifest_list=*/"delta-manifest-analyze",
+ /*delta_manifest_list_size=*/200,
+ /*changelog_manifest_list=*/std::nullopt,
+ /*changelog_manifest_list_size=*/std::nullopt,
+ /*index_manifest=*/std::nullopt,
+ /*commit_user=*/"analyze-user",
+ /*commit_identifier=*/42,
+ /*commit_kind=*/Snapshot::CommitKind::Analyze(),
+ /*time_millis=*/1700000000000ll,
+ /*log_offsets=*/std::map<int32_t, int64_t>(),
+ /*total_record_count=*/0,
+ /*delta_record_count=*/0,
+ /*changelog_record_count=*/0,
+ /*watermark=*/std::nullopt,
+ /*statistics=*/"test-statistics",
+ /*properties=*/std::nullopt,
+ /*next_row_id=*/std::nullopt);
+
+ ASSERT_EQ(Snapshot::CommitKind::Analyze(), snapshot.GetCommitKind());
+ ASSERT_EQ("ANALYZE",
Snapshot::CommitKind::ToString(snapshot.GetCommitKind()));
+}
+
+TEST_F(SnapshotTest, TestCommitKindAnalyzeSerializeAndDeserialize) {
+ std::string json_str = R"({
+ "version" : 3,
+ "id" : 20,
+ "schemaId" : 5,
+ "baseManifestList" : "base-manifest-analyze",
+ "baseManifestListSize" : 100,
+ "deltaManifestList" : "delta-manifest-analyze",
+ "deltaManifestListSize" : 200,
+ "changelogManifestList" : null,
+ "commitUser" : "analyze-user",
+ "commitIdentifier" : 42,
+ "commitKind" : "ANALYZE",
+ "timeMillis" : 1700000000000,
+ "logOffsets" : { },
+ "totalRecordCount" : 0,
+ "deltaRecordCount" : 0,
+ "changelogRecordCount" : 0,
+ "statistics" : "test-statistics"
+ })";
+
+ ASSERT_OK_AND_ASSIGN(Snapshot snapshot,
Snapshot::FromJsonString(json_str));
+
+ // Verify deserialization
+ ASSERT_EQ(20, snapshot.Id());
+ ASSERT_EQ(5, snapshot.SchemaId());
+ ASSERT_EQ(Snapshot::CommitKind::Analyze(), snapshot.GetCommitKind());
+ ASSERT_EQ("test-statistics", snapshot.Statistics().value());
+
+ // Verify round-trip serialization
+ ASSERT_OK_AND_ASSIGN(std::string serialized, snapshot.ToJsonString());
+ ASSERT_EQ(ReplaceAll(json_str), ReplaceAll(serialized));
+
+ // Verify re-deserialization produces equal snapshot
+ ASSERT_OK_AND_ASSIGN(Snapshot deserialized,
Snapshot::FromJsonString(serialized));
+ ASSERT_EQ(snapshot, deserialized);
+}
+
+TEST_F(SnapshotTest, TestCommitKindToStringAndFromString) {
+ // Verify all CommitKind values round-trip through ToString/FromString
+ ASSERT_EQ("APPEND",
Snapshot::CommitKind::ToString(Snapshot::CommitKind::Append()));
+ ASSERT_EQ("COMPACT",
Snapshot::CommitKind::ToString(Snapshot::CommitKind::Compact()));
+ ASSERT_EQ("OVERWRITE",
Snapshot::CommitKind::ToString(Snapshot::CommitKind::Overwrite()));
+ ASSERT_EQ("ANALYZE",
Snapshot::CommitKind::ToString(Snapshot::CommitKind::Analyze()));
+
+ ASSERT_EQ(Snapshot::CommitKind::Append(),
Snapshot::CommitKind::FromString("APPEND"));
+ ASSERT_EQ(Snapshot::CommitKind::Compact(),
Snapshot::CommitKind::FromString("COMPACT"));
+ ASSERT_EQ(Snapshot::CommitKind::Overwrite(),
Snapshot::CommitKind::FromString("OVERWRITE"));
+ ASSERT_EQ(Snapshot::CommitKind::Analyze(),
Snapshot::CommitKind::FromString("ANALYZE"));
+
+ // Verify equality/inequality
+ ASSERT_FALSE(Snapshot::CommitKind::Analyze() ==
Snapshot::CommitKind::Append());
+ ASSERT_FALSE(Snapshot::CommitKind::Analyze() ==
Snapshot::CommitKind::Compact());
+ ASSERT_FALSE(Snapshot::CommitKind::Analyze() ==
Snapshot::CommitKind::Overwrite());
+ ASSERT_TRUE(Snapshot::CommitKind::Analyze() ==
Snapshot::CommitKind::Analyze());
+}
+
+TEST_F(SnapshotTest, TestSnapshotInfoCommitKindToString) {
+ ASSERT_EQ("APPEND",
SnapshotInfo::CommitKindToString(SnapshotInfo::CommitKind::APPEND));
+ ASSERT_EQ("COMPACT",
SnapshotInfo::CommitKindToString(SnapshotInfo::CommitKind::COMPACT));
+ ASSERT_EQ("OVERWRITE",
SnapshotInfo::CommitKindToString(SnapshotInfo::CommitKind::OVERWRITE));
+ ASSERT_EQ("ANALYZE",
SnapshotInfo::CommitKindToString(SnapshotInfo::CommitKind::ANALYZE));
+ ASSERT_EQ("UNKNOWN",
SnapshotInfo::CommitKindToString(SnapshotInfo::CommitKind::UNKNOWN));
+}
+
+TEST_F(SnapshotTest, TestChangelogManifestListSerialization) {
+ // Test with changelog_manifest_list set to a non-null value
+ {
+ std::string json_str = R"({
+ "version" : 3,
+ "id" : 1,
+ "schemaId" : 0,
+ "baseManifestList" : "base-manifest-list",
+ "deltaManifestList" : "delta-manifest-list",
+ "changelogManifestList" : "changelog-manifest-list",
+ "changelogManifestListSize" : 42,
+ "commitUser" : "user-01",
+ "commitIdentifier" : 100,
+ "commitKind" : "APPEND",
+ "timeMillis" : 1700000000000,
+ "logOffsets" : { },
+ "totalRecordCount" : 10,
+ "deltaRecordCount" : 5,
+ "changelogRecordCount" : 3
+ })";
+
+ ASSERT_OK_AND_ASSIGN(Snapshot snapshot,
Snapshot::FromJsonString(json_str));
+ ASSERT_EQ("changelog-manifest-list",
snapshot.ChangelogManifestList().value());
+ ASSERT_EQ(42, snapshot.ChangelogManifestListSize().value());
+
+ ASSERT_OK_AND_ASSIGN(std::string serialized, snapshot.ToJsonString());
+ ASSERT_EQ(ReplaceAll(json_str), ReplaceAll(serialized));
+
+ // Verify round-trip
+ ASSERT_OK_AND_ASSIGN(Snapshot deserialized,
Snapshot::FromJsonString(serialized));
+ ASSERT_EQ(snapshot, deserialized);
+ }
+
+ // Test with changelog_manifest_list set to null
+ {
+ std::string json_str = R"({
+ "version" : 3,
+ "id" : 2,
+ "schemaId" : 0,
+ "baseManifestList" : "base-manifest-list",
+ "deltaManifestList" : "delta-manifest-list",
+ "changelogManifestList" : null,
+ "commitUser" : "user-02",
+ "commitIdentifier" : 200,
+ "commitKind" : "COMPACT",
+ "timeMillis" : 1700000001000,
+ "logOffsets" : { },
+ "totalRecordCount" : 20,
+ "deltaRecordCount" : 10,
+ "changelogRecordCount" : 0
+ })";
+
+ ASSERT_OK_AND_ASSIGN(Snapshot snapshot,
Snapshot::FromJsonString(json_str));
+ ASSERT_EQ(std::nullopt, snapshot.ChangelogManifestList());
+ ASSERT_EQ(std::nullopt, snapshot.ChangelogManifestListSize());
+
+ ASSERT_OK_AND_ASSIGN(std::string serialized, snapshot.ToJsonString());
+ ASSERT_EQ(ReplaceAll(json_str), ReplaceAll(serialized));
+
+ // Verify round-trip
+ ASSERT_OK_AND_ASSIGN(Snapshot deserialized,
Snapshot::FromJsonString(serialized));
+ ASSERT_EQ(snapshot, deserialized);
+ }
+}
+
+} // namespace paimon::test
diff --git a/src/paimon/core/table/bucket_mode.h
b/src/paimon/core/table/bucket_mode.h
new file mode 100644
index 0000000..2891dac
--- /dev/null
+++ b/src/paimon/core/table/bucket_mode.h
@@ -0,0 +1,67 @@
+/*
+ * 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>
+
+namespace paimon {
+
+/// Bucket mode of the table, it affects the writing process and also affects
the data skipping in
+/// reading.
+enum class BucketMode {
+ /// The fixed number of buckets configured by the user can only be
modified through offline
+ /// commands. The data is distributed to the corresponding buckets
according to the hash value
+ /// of
+ /// bucket key (default is primary key), and the reading end can perform
bucket skipping based
+ /// on
+ /// the filtering conditions of the bucket key.
+ HASH_FIXED = 0,
+
+ /// The dynamic bucket mode records which bucket the key corresponds to
through the index files.
+ /// The index records the correspondence between the hash value of the
primary-key and the
+ /// bucket. This mode cannot support multiple concurrent writes or bucket
skipping for reading
+ /// filter conditions. This mode only works for changelog table.
+ HASH_DYNAMIC,
+
+ /// The cross partition mode is for cross partition upsert (primary keys
not contain all
+ /// partition fields). It directly maintains the mapping of primary keys
to partition and
+ /// bucket,
+ /// uses local disks, and initializes indexes by reading all existing keys
in the table when
+ /// starting stream write job.
+ CROSS_PARTITION,
+
+ /// Ignoring bucket concept, although all data is written to bucket-0, the
parallelism of reads
+ /// and writes is unrestricted. This mode only works for append-only table.
+ BUCKET_UNAWARE,
+
+ /// Configured by 'bucket' = '-2' (postpone bucket) for primary key table.
This mode aims to
+ /// solve the difficulty to determine a fixed number of buckets and
support different buckets
+ /// for
+ /// different partitions. The bucket will be adaptively adjusted to the
appropriate value in the
+ /// background.
+ POSTPONE_MODE
+};
+
+class BucketModeDefine {
+ public:
+ static constexpr int32_t UNAWARE_BUCKET = 0;
+ static constexpr int32_t POSTPONE_BUCKET = -2;
+};
+
+} // namespace paimon
diff --git a/src/paimon/core/table/table.cpp b/src/paimon/core/table/table.cpp
new file mode 100644
index 0000000..557c452
--- /dev/null
+++ b/src/paimon/core/table/table.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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/catalog/table.h"
+
+#include <optional>
+
+#include "fmt/format.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.h"
+#include "paimon/fs/file_system.h"
+
+namespace paimon {
+
+Result<std::shared_ptr<Table>> Table::Create(const
std::shared_ptr<FileSystem>& file_system,
+ const std::string& table_path,
+ const Identifier& identifier) {
+ PAIMON_ASSIGN_OR_RAISE(bool exist, file_system->Exists(table_path));
+ if (!exist) {
+ return Status::NotExist(fmt::format("{} not exist",
identifier.ToString()));
+ }
+
+ SchemaManager schema_manager(file_system, table_path);
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>>
latest_schema,
+ schema_manager.Latest());
+ if (!latest_schema) {
+ return Status::NotExist(
+ fmt::format("load table schema for {} failed",
identifier.ToString()));
+ }
+
+ auto schema = std::static_pointer_cast<Schema>(*latest_schema);
+ return std::make_shared<Table>(schema, identifier.GetDatabaseName(),
identifier.GetTableName());
+}
+
+std::string Table::FullName() const {
+ return database_ == Identifier::kUnknownDatabase ? table_name_ : database_
+ "." + table_name_;
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/table/table_test.cpp
b/src/paimon/core/table/table_test.cpp
new file mode 100644
index 0000000..f2d12f2
--- /dev/null
+++ b/src/paimon/core/table/table_test.cpp
@@ -0,0 +1,91 @@
+/*
+ * 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/catalog/table.h"
+
+#include "arrow/api.h"
+#include "gtest/gtest.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.h"
+#include "paimon/schema/schema.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(TableTest, TestCreateWithUnknownDatabase) {
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+
+ SchemaManager schema_manager(dir->GetFileSystem(), dir->Str());
+ auto schema =
+ arrow::schema({arrow::field("id", arrow::int32(), /*nullable=*/false),
+ arrow::field("name", arrow::utf8()),
arrow::field("value", arrow::int64())});
+ std::vector<std::string> partition_keys = {"name"};
+ std::vector<std::string> primary_keys = {"id"};
+ std::map<std::string, std::string> options = {
+ {"file.format", "orc"},
+ {"commit.force-compact", "true"},
+ };
+
+ ASSERT_OK_AND_ASSIGN([[maybe_unused]] std::unique_ptr<TableSchema>
created_schema,
+ schema_manager.CreateTable(schema, partition_keys,
primary_keys, options));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Table> table,
+ Table::Create(dir->GetFileSystem(), dir->Str(),
Identifier("tbl1")));
+
+ EXPECT_EQ(table->Name(), "tbl1");
+ EXPECT_EQ(table->FullName(), "tbl1");
+
+ std::shared_ptr<Schema> latest_schema = table->LatestSchema();
+ ASSERT_NE(latest_schema, nullptr);
+ auto data_schema = std::dynamic_pointer_cast<DataSchema>(latest_schema);
+ ASSERT_TRUE(data_schema != nullptr);
+ ASSERT_TRUE(std::dynamic_pointer_cast<TableSchema>(latest_schema) !=
nullptr);
+ EXPECT_EQ(data_schema->Id(), 0);
+ EXPECT_EQ(data_schema->PartitionKeys(), partition_keys);
+ EXPECT_EQ(data_schema->PrimaryKeys(), primary_keys);
+}
+
+TEST(TableTest, TestCreateFailedWithNonExistSchema) {
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto fs = dir->GetFileSystem();
+ SchemaManager schema_manager(fs, dir->Str());
+ auto schema =
+ arrow::schema({arrow::field("id", arrow::int32(), /*nullable=*/false),
+ arrow::field("name", arrow::utf8()),
arrow::field("value", arrow::int64())});
+ std::vector<std::string> partition_keys = {"name"};
+ std::vector<std::string> primary_keys = {"id"};
+ std::map<std::string, std::string> options = {
+ {"file.format", "orc"},
+ {"commit.force-compact", "true"},
+ };
+
+ ASSERT_OK_AND_ASSIGN([[maybe_unused]] std::unique_ptr<TableSchema>
created_schema,
+ schema_manager.CreateTable(schema, partition_keys,
primary_keys, options));
+
+ // remove schema
+ std::string schema_path = schema_manager.ToSchemaPath(0);
+ ASSERT_OK(fs->Delete(schema_path, /*recursive=*/false));
+ // check create table failed
+ ASSERT_NOK_WITH_MSG(
+ Table::Create(fs, dir->Str(), Identifier("tbl1")),
+ "load table schema for Identifier{database='unknown', table='tbl1'}
failed");
+}
+
+} // namespace paimon::test