This is an automated email from the ASF dual-hosted git repository.
fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new c343e94 feat: snapshot serde (#74)
c343e94 is described below
commit c343e94bc17945282a5d31bda3856c51c5992410
Author: Junwang Zhao <[email protected]>
AuthorDate: Wed Apr 16 04:33:53 2025 +0800
feat: snapshot serde (#74)
Signed-off-by: Junwang Zhao <[email protected]>
---
src/iceberg/json_internal.cc | 187 +++++++++++++++++++++++++++++++++++++++++++
src/iceberg/json_internal.h | 25 ++++++
src/iceberg/snapshot.cc | 28 +++++++
src/iceberg/snapshot.h | 63 ++++++++++++++-
src/iceberg/type_fwd.h | 4 +-
test/json_internal_test.cc | 122 ++++++++++++++++++++++++++--
6 files changed, 418 insertions(+), 11 deletions(-)
diff --git a/src/iceberg/json_internal.cc b/src/iceberg/json_internal.cc
index d69b462..ec34be1 100644
--- a/src/iceberg/json_internal.cc
+++ b/src/iceberg/json_internal.cc
@@ -19,8 +19,10 @@
#include "iceberg/json_internal.h"
+#include <cstdint>
#include <format>
#include <regex>
+#include <unordered_set>
#include <nlohmann/json.hpp>
@@ -28,6 +30,7 @@
#include "iceberg/result.h"
#include "iceberg/schema.h"
#include "iceberg/schema_internal.h"
+#include "iceberg/snapshot.h"
#include "iceberg/sort_order.h"
#include "iceberg/transform.h"
#include "iceberg/type.h"
@@ -70,6 +73,55 @@ constexpr std::string_view kValueRequired = "value-required";
constexpr std::string_view kFieldId = "field-id";
constexpr std::string_view kSpecId = "spec-id";
+constexpr std::string_view kSnapshotId = "snapshot-id";
+constexpr std::string_view kParentSnapshotId = "parent-snapshot-id";
+constexpr std::string_view kSequenceNumber = "sequence-number";
+constexpr std::string_view kTimestampMs = "timestamp-ms";
+constexpr std::string_view kManifestList = "manifest-list";
+constexpr std::string_view kSummary = "summary";
+constexpr std::string_view kMinSnapshotsToKeep = "min-snapshots-to-keep";
+constexpr std::string_view kMaxSnapshotAgeMs = "max-snapshot-age-ms";
+constexpr std::string_view kMaxRefAgeMs = "max-ref-age-ms";
+
+constexpr int64_t kInitialSequenceNumber = 0;
+
+const std::unordered_set<std::string_view> kValidSnapshotSummaryFields = {
+ SnapshotSummaryFields::kOperation,
+ SnapshotSummaryFields::kAddedDataFiles,
+ SnapshotSummaryFields::kDeletedDataFiles,
+ SnapshotSummaryFields::kTotalDataFiles,
+ SnapshotSummaryFields::kAddedDeleteFiles,
+ SnapshotSummaryFields::kAddedEqDeleteFiles,
+ SnapshotSummaryFields::kRemovedEqDeleteFiles,
+ SnapshotSummaryFields::kAddedPosDeleteFiles,
+ SnapshotSummaryFields::kRemovedPosDeleteFiles,
+ SnapshotSummaryFields::kAddedDVs,
+ SnapshotSummaryFields::kRemovedDVs,
+ SnapshotSummaryFields::kRemovedDeleteFiles,
+ SnapshotSummaryFields::kTotalDeleteFiles,
+ SnapshotSummaryFields::kAddedRecords,
+ SnapshotSummaryFields::kDeletedRecords,
+ SnapshotSummaryFields::kTotalRecords,
+ SnapshotSummaryFields::kAddedFileSize,
+ SnapshotSummaryFields::kRemovedFileSize,
+ SnapshotSummaryFields::kTotalFileSize,
+ SnapshotSummaryFields::kAddedPosDeletes,
+ SnapshotSummaryFields::kRemovedPosDeletes,
+ SnapshotSummaryFields::kTotalPosDeletes,
+ SnapshotSummaryFields::kAddedEqDeletes,
+ SnapshotSummaryFields::kRemovedEqDeletes,
+ SnapshotSummaryFields::kTotalEqDeletes,
+ SnapshotSummaryFields::kDeletedDuplicatedFiles,
+ SnapshotSummaryFields::kChangedPartitionCountProp,
+ SnapshotSummaryFields::kWAPId,
+ SnapshotSummaryFields::kPublishedWAPId,
+ SnapshotSummaryFields::kSourceSnapshotId,
+ SnapshotSummaryFields::kEngineName,
+ SnapshotSummaryFields::kEngineVersion};
+
+const std::unordered_set<std::string_view> kValidDataOperation = {
+ DataOperation::kAppend, DataOperation::kReplace, DataOperation::kOverwrite,
+ DataOperation::kDelete};
template <typename T>
Result<T> GetJsonValue(const nlohmann::json& json, std::string_view key) {
@@ -89,6 +141,30 @@ Result<T> GetJsonValue(const nlohmann::json& json,
std::string_view key) {
}
}
+template <typename T>
+Result<std::optional<T>> GetJsonValueOptional(const nlohmann::json& json,
+ std::string_view key) {
+ if (!json.contains(key)) {
+ return std::nullopt;
+ }
+ try {
+ return json.at(key).get<T>();
+ } catch (const std::exception& ex) {
+ return unexpected<Error>({
+ .kind = ErrorKind::kJsonParseError,
+ .message = std::format("Failed to parse key '{}' in {}", key,
json.dump()),
+ });
+ }
+}
+
+template <typename T>
+void SetOptionalField(nlohmann::json& json, std::string_view key,
+ const std::optional<T>& value) {
+ if (value.has_value()) {
+ json[key] = *value;
+ }
+}
+
} // namespace
nlohmann::json ToJson(const SortField& sort_field) {
@@ -231,6 +307,39 @@ nlohmann::json SchemaToJson(const Schema& schema) {
return json;
}
+nlohmann::json ToJson(const SnapshotRef& ref) {
+ nlohmann::json json;
+ json[kSnapshotId] = ref.snapshot_id;
+ json[kType] = SnapshotRefTypeToString(ref.type());
+ if (ref.type() == SnapshotRefType::kBranch) {
+ const auto& branch = std::get<SnapshotRef::Branch>(ref.retention);
+ SetOptionalField(json, kMinSnapshotsToKeep, branch.min_snapshots_to_keep);
+ SetOptionalField(json, kMaxSnapshotAgeMs, branch.max_snapshot_age_ms);
+ SetOptionalField(json, kMaxRefAgeMs, branch.max_ref_age_ms);
+ } else if (ref.type() == SnapshotRefType::kTag) {
+ const auto& tag = std::get<SnapshotRef::Tag>(ref.retention);
+ SetOptionalField(json, kMaxRefAgeMs, tag.max_ref_age_ms);
+ }
+ return json;
+}
+
+nlohmann::json ToJson(const Snapshot& snapshot) {
+ nlohmann::json json;
+ json[kSnapshotId] = snapshot.snapshot_id;
+ SetOptionalField(json, kParentSnapshotId, snapshot.parent_snapshot_id);
+ if (snapshot.sequence_number > kInitialSequenceNumber) {
+ json[kSequenceNumber] = snapshot.sequence_number;
+ }
+ json[kTimestampMs] = snapshot.timestamp_ms;
+ json[kManifestList] = snapshot.manifest_list;
+ // If there is an operation, write the summary map
+ if (snapshot.operation().has_value()) {
+ json[kSummary] = snapshot.summary;
+ }
+ SetOptionalField(json, kSchemaId, snapshot.schema_id);
+ return json;
+}
+
namespace {
Result<std::unique_ptr<Type>> StructTypeFromJson(const nlohmann::json& json) {
@@ -419,4 +528,82 @@ Result<std::unique_ptr<PartitionSpec>>
PartitionSpecFromJson(
return std::make_unique<PartitionSpec>(schema, spec_id,
std::move(partition_fields));
}
+Result<std::unique_ptr<SnapshotRef>> SnapshotRefFromJson(const nlohmann::json&
json) {
+ ICEBERG_ASSIGN_OR_RAISE(auto snapshot_id, GetJsonValue<int64_t>(json,
kSnapshotId));
+ ICEBERG_ASSIGN_OR_RAISE(
+ auto type,
+ GetJsonValue<std::string>(json,
kType).and_then(SnapshotRefTypeFromString));
+ if (type == SnapshotRefType::kBranch) {
+ ICEBERG_ASSIGN_OR_RAISE(auto min_snapshots_to_keep,
+ GetJsonValueOptional<int32_t>(json,
kMinSnapshotsToKeep));
+ ICEBERG_ASSIGN_OR_RAISE(auto max_snapshot_age_ms,
+ GetJsonValueOptional<int64_t>(json,
kMaxSnapshotAgeMs));
+ ICEBERG_ASSIGN_OR_RAISE(auto max_ref_age_ms,
+ GetJsonValueOptional<int64_t>(json, kMaxRefAgeMs));
+
+ return std::make_unique<SnapshotRef>(
+ snapshot_id, SnapshotRef::Branch{.min_snapshots_to_keep =
min_snapshots_to_keep,
+ .max_snapshot_age_ms =
max_snapshot_age_ms,
+ .max_ref_age_ms = max_ref_age_ms});
+ } else {
+ ICEBERG_ASSIGN_OR_RAISE(auto max_ref_age_ms,
+ GetJsonValueOptional<int64_t>(json, kMaxRefAgeMs));
+
+ return std::make_unique<SnapshotRef>(
+ snapshot_id, SnapshotRef::Tag{.max_ref_age_ms = max_ref_age_ms});
+ }
+}
+
+Result<std::unique_ptr<Snapshot>> SnapshotFromJson(const nlohmann::json& json)
{
+ ICEBERG_ASSIGN_OR_RAISE(auto snapshot_id, GetJsonValue<int64_t>(json,
kSnapshotId));
+ ICEBERG_ASSIGN_OR_RAISE(auto sequence_number,
+ GetJsonValueOptional<int64_t>(json,
kSequenceNumber));
+ ICEBERG_ASSIGN_OR_RAISE(auto timestamp_ms, GetJsonValue<int64_t>(json,
kTimestampMs));
+ ICEBERG_ASSIGN_OR_RAISE(auto manifest_list,
+ GetJsonValue<std::string>(json, kManifestList));
+
+ ICEBERG_ASSIGN_OR_RAISE(auto parent_snapshot_id,
+ GetJsonValueOptional<int64_t>(json,
kParentSnapshotId));
+
+ ICEBERG_ASSIGN_OR_RAISE(auto summary_json,
+ GetJsonValueOptional<nlohmann::json>(json,
kSummary));
+ std::unordered_map<std::string, std::string> summary;
+ if (summary_json.has_value()) {
+ for (const auto& [key, value] : summary_json->items()) {
+ if (!kValidSnapshotSummaryFields.contains(key)) {
+ return unexpected<Error>({
+ .kind = ErrorKind::kJsonParseError,
+ .message = std::format("Invalid snapshot summary field: {}", key),
+ });
+ }
+ if (!value.is_string()) {
+ return unexpected<Error>({
+ .kind = ErrorKind::kJsonParseError,
+ .message =
+ std::format("Invalid snapshot summary field value: {}",
value.dump()),
+ });
+ }
+ if (key == SnapshotSummaryFields::kOperation &&
+ !kValidDataOperation.contains(value.get<std::string>())) {
+ return unexpected<Error>({
+ .kind = ErrorKind::kJsonParseError,
+ .message = std::format("Invalid snapshot operation: {}",
value.dump()),
+ });
+ }
+ summary[key] = value.get<std::string>();
+ }
+ // If summary is available but operation is missing, set operation to
overwrite.
+ if (!summary.contains(SnapshotSummaryFields::kOperation)) {
+ summary[SnapshotSummaryFields::kOperation] = DataOperation::kOverwrite;
+ }
+ }
+
+ ICEBERG_ASSIGN_OR_RAISE(auto schema_id, GetJsonValueOptional<int32_t>(json,
kSchemaId));
+
+ return std::make_unique<Snapshot>(
+ snapshot_id, parent_snapshot_id,
+ sequence_number.has_value() ? *sequence_number : kInitialSequenceNumber,
+ timestamp_ms, manifest_list, std::move(summary), schema_id);
+}
+
} // namespace iceberg
diff --git a/src/iceberg/json_internal.h b/src/iceberg/json_internal.h
index 4e754fd..450578f 100644
--- a/src/iceberg/json_internal.h
+++ b/src/iceberg/json_internal.h
@@ -88,6 +88,18 @@ nlohmann::json TypeToJson(const Type& type);
/// \return The JSON representation of the field.
nlohmann::json FieldToJson(const SchemaField& field);
+/// \brief Serializes a `SnapshotRef` object to JSON.
+///
+/// \param[in] snapshot_ref The `SnapshotRef` object to be serialized.
+/// \return A JSON object representing the `SnapshotRef`.
+nlohmann::json ToJson(const SnapshotRef& snapshot_ref);
+
+/// \brief Serializes a `Snapshot` object to JSON.
+///
+/// \param[in] snapshot The `Snapshot` object to be serialized.
+/// \return A JSON object representing the `snapshot`.
+nlohmann::json ToJson(const Snapshot& snapshot);
+
/// \brief Convert JSON to an Iceberg Schema.
///
/// \param[in] json The JSON representation of the schema.
@@ -153,4 +165,17 @@ Result<std::unique_ptr<PartitionField>>
PartitionFieldFromJson(
/// the JSON is malformed or missing expected fields, an error will be
returned.
Result<std::unique_ptr<PartitionSpec>> PartitionSpecFromJson(
const std::shared_ptr<Schema>& schema, const nlohmann::json& json);
+
+/// \brief Deserializes a JSON object into a `SnapshotRef` object.
+///
+/// \param[in] json The JSON object representing a `SnapshotRef`.
+/// \return A `SnapshotRef` object or an error if the conversion fails.
+Result<std::unique_ptr<SnapshotRef>> SnapshotRefFromJson(const nlohmann::json&
json);
+
+/// \brief Deserializes a JSON object into a `Snapshot` object.
+///
+/// \param[in] json The JSON representation of the snapshot.
+/// \return A `Snapshot` object or an error if the conversion fails.
+Result<std::unique_ptr<Snapshot>> SnapshotFromJson(const nlohmann::json& json);
+
} // namespace iceberg
diff --git a/src/iceberg/snapshot.cc b/src/iceberg/snapshot.cc
index a17d9e8..fb994f8 100644
--- a/src/iceberg/snapshot.cc
+++ b/src/iceberg/snapshot.cc
@@ -21,6 +21,16 @@
namespace iceberg {
+bool SnapshotRef::Branch::Equals(const SnapshotRef::Branch& other) const {
+ return min_snapshots_to_keep == other.min_snapshots_to_keep &&
+ max_snapshot_age_ms == other.max_snapshot_age_ms &&
+ max_ref_age_ms == other.max_ref_age_ms;
+}
+
+bool SnapshotRef::Tag::Equals(const SnapshotRef::Tag& other) const {
+ return max_ref_age_ms == other.max_ref_age_ms;
+}
+
SnapshotRefType SnapshotRef::type() const noexcept {
return std::visit(
[&](const auto& retention) -> SnapshotRefType {
@@ -34,6 +44,24 @@ SnapshotRefType SnapshotRef::type() const noexcept {
retention);
}
+bool SnapshotRef::Equals(const SnapshotRef& other) const {
+ if (this == &other) {
+ return true;
+ }
+ if (type() != other.type()) {
+ return false;
+ }
+
+ if (type() == SnapshotRefType::kBranch) {
+ return snapshot_id == other.snapshot_id &&
+ std::get<Branch>(retention) == std::get<Branch>(other.retention);
+
+ } else {
+ return snapshot_id == other.snapshot_id &&
+ std::get<Tag>(retention) == std::get<Tag>(other.retention);
+ }
+}
+
std::optional<std::string_view> Snapshot::operation() const {
auto it = summary.find(SnapshotSummaryFields::kOperation);
if (it != summary.end()) {
diff --git a/src/iceberg/snapshot.h b/src/iceberg/snapshot.h
index 29577e9..da9dd95 100644
--- a/src/iceberg/snapshot.h
+++ b/src/iceberg/snapshot.h
@@ -26,6 +26,7 @@
#include <variant>
#include "iceberg/iceberg_export.h"
+#include "iceberg/result.h"
namespace iceberg {
@@ -39,6 +40,26 @@ enum class SnapshotRefType {
kTag,
};
+/// \brief Get the relative snapshot reference type name
+ICEBERG_EXPORT constexpr std::string_view SnapshotRefTypeToString(
+ SnapshotRefType type) noexcept {
+ switch (type) {
+ case SnapshotRefType::kBranch:
+ return "branch";
+ case SnapshotRefType::kTag:
+ return "tag";
+ }
+}
+/// \brief Get the relative snapshot reference type from name
+ICEBERG_EXPORT constexpr Result<SnapshotRefType> SnapshotRefTypeFromString(
+ std::string_view str) noexcept {
+ if (str == "branch") return SnapshotRefType::kBranch;
+ if (str == "tag") return SnapshotRefType::kTag;
+ return unexpected<Error>(
+ {.kind = ErrorKind::kInvalidArgument,
+ .message = "Invalid snapshot reference type: {}" + std::string(str)});
+}
+
/// \brief A reference to a snapshot, either a branch or a tag.
struct ICEBERG_EXPORT SnapshotRef {
struct ICEBERG_EXPORT Branch {
@@ -54,6 +75,18 @@ struct ICEBERG_EXPORT SnapshotRef {
/// of the snapshot reference to keep while expiring snapshots. Defaults
to table
/// property history.expire.max-ref-age-ms. The main branch never expires.
std::optional<int64_t> max_ref_age_ms;
+
+ /// \brief Compare two branches for equality.
+ friend bool operator==(const Branch& lhs, const Branch& rhs) {
+ return lhs.Equals(rhs);
+ }
+
+ /// \brief Compare two branches for inequality.
+ friend bool operator!=(const Branch& lhs, const Branch& rhs) { return
!(lhs == rhs); }
+
+ private:
+ /// \brief Compare two branches for equality.
+ bool Equals(const Branch& other) const;
};
struct ICEBERG_EXPORT Tag {
@@ -61,6 +94,16 @@ struct ICEBERG_EXPORT SnapshotRef {
/// of the snapshot reference to keep while expiring snapshots. Defaults
to table
/// property history.expire.max-ref-age-ms. The main branch never expires.
std::optional<int64_t> max_ref_age_ms;
+
+ /// \brief Compare two tags for equality.
+ friend bool operator==(const Tag& lhs, const Tag& rhs) { return
lhs.Equals(rhs); }
+
+ /// \brief Compare two tags for inequality.
+ friend bool operator!=(const Tag& lhs, const Tag& rhs) { return !(lhs ==
rhs); }
+
+ private:
+ /// \brief Compare two tags for equality.
+ bool Equals(const Tag& other) const;
};
/// A reference's snapshot ID. The tagged snapshot or latest snapshot of a
branch.
@@ -69,6 +112,20 @@ struct ICEBERG_EXPORT SnapshotRef {
std::variant<Branch, Tag> retention;
SnapshotRefType type() const noexcept;
+
+ /// \brief Compare two snapshot refs for equality
+ friend bool operator==(const SnapshotRef& lhs, const SnapshotRef& rhs) {
+ return lhs.Equals(rhs);
+ }
+
+ /// \brief Compare two snapshot refs for inequality.
+ friend bool operator!=(const SnapshotRef& lhs, const SnapshotRef& rhs) {
+ return !(lhs == rhs);
+ }
+
+ private:
+ /// \brief Compare two snapshot refs for equality.
+ bool Equals(const SnapshotRef& other) const;
};
/// \brief Optional Snapshot Summary Fields
@@ -139,11 +196,11 @@ struct SnapshotSummaryFields {
/// Other Fields, see https://iceberg.apache.org/spec/#other-fields
/// \brief The Write-Audit-Publish id of a staged snapshot
- inline static const std::string kWAPID = "wap.id";
+ inline static const std::string kWAPId = "wap.id";
/// \brief The Write-Audit-Publish id of a snapshot already been published
- inline static const std::string kPublishedWAPID = "published-wap-id";
+ inline static const std::string kPublishedWAPId = "published-wap-id";
/// \brief The original id of a cherry-picked snapshot
- inline static const std::string kSourceSnapshotID = "source-snapshot-id";
+ inline static const std::string kSourceSnapshotId = "source-snapshot-id";
/// \brief Name of the engine that created the snapshot
inline static const std::string kEngineName = "engine-name";
/// \brief Version of the engine that created the snapshot
diff --git a/src/iceberg/type_fwd.h b/src/iceberg/type_fwd.h
index 519164e..44db1c0 100644
--- a/src/iceberg/type_fwd.h
+++ b/src/iceberg/type_fwd.h
@@ -97,10 +97,12 @@ class Transaction;
class HistoryEntry;
class Snapshot;
+struct SnapshotRef;
+enum class SnapshotRefType;
class SortField;
class SortOrder;
class StructLike;
-class TableMetadata;
+struct TableMetadata;
class Transform;
enum class TransformType;
class TransformFunction;
diff --git a/test/json_internal_test.cc b/test/json_internal_test.cc
index 10b25f5..6b6a220 100644
--- a/test/json_internal_test.cc
+++ b/test/json_internal_test.cc
@@ -19,42 +19,54 @@
#include "iceberg/json_internal.h"
-#include <format>
#include <memory>
#include <gtest/gtest.h>
+#include <iceberg/result.h>
#include <nlohmann/json.hpp>
+#include "gmock/gmock.h"
#include "iceberg/partition_spec.h"
#include "iceberg/schema.h"
+#include "iceberg/snapshot.h"
#include "iceberg/sort_field.h"
#include "iceberg/sort_order.h"
#include "iceberg/transform.h"
#include "iceberg/util/formatter.h" // IWYU pragma: keep
+#include "matchers.h"
namespace iceberg {
namespace {
// Specialized FromJson helper based on type
template <typename T>
-expected<std::unique_ptr<T>, Error> FromJsonHelper(const nlohmann::json& json);
+Result<std::unique_ptr<T>> FromJsonHelper(const nlohmann::json& json);
template <>
-expected<std::unique_ptr<SortField>, Error> FromJsonHelper(const
nlohmann::json& json) {
+Result<std::unique_ptr<SortField>> FromJsonHelper(const nlohmann::json& json) {
return SortFieldFromJson(json);
}
template <>
-expected<std::unique_ptr<SortOrder>, Error> FromJsonHelper(const
nlohmann::json& json) {
+Result<std::unique_ptr<SortOrder>> FromJsonHelper(const nlohmann::json& json) {
return SortOrderFromJson(json);
}
template <>
-expected<std::unique_ptr<PartitionField>, Error> FromJsonHelper(
- const nlohmann::json& json) {
+Result<std::unique_ptr<PartitionField>> FromJsonHelper(const nlohmann::json&
json) {
return PartitionFieldFromJson(json);
}
+template <>
+Result<std::unique_ptr<SnapshotRef>> FromJsonHelper(const nlohmann::json&
json) {
+ return SnapshotRefFromJson(json);
+}
+
+template <>
+Result<std::unique_ptr<Snapshot>> FromJsonHelper(const nlohmann::json& json) {
+ return SnapshotFromJson(json);
+}
+
// Helper function to reduce duplication in testing
template <typename T>
void TestJsonConversion(const T& obj, const nlohmann::json& expected_json) {
@@ -116,7 +128,8 @@ TEST(JsonPartitionTest, PartitionFieldFromJsonMissingField)
{
auto result = PartitionFieldFromJson(invalid_json);
EXPECT_FALSE(result.has_value());
- EXPECT_EQ(result.error().kind, ErrorKind::kJsonParseError);
+ EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+ EXPECT_THAT(result, HasErrorMessage("Missing 'source-id'"));
}
TEST(JsonPartitionTest, PartitionSpec) {
@@ -148,4 +161,99 @@ TEST(JsonPartitionTest, PartitionSpec) {
EXPECT_EQ(spec, *parsed_spec_result.value());
}
+TEST(JsonInternalTest, SnapshotRefBranch) {
+ SnapshotRef ref(1234567890, SnapshotRef::Branch{.min_snapshots_to_keep = 10,
+ .max_snapshot_age_ms =
123456789,
+ .max_ref_age_ms =
987654321});
+
+ // Create a JSON object with the expected values
+ nlohmann::json expected_json =
+ R"({"snapshot-id":1234567890,
+ "type":"branch",
+ "min-snapshots-to-keep":10,
+ "max-snapshot-age-ms":123456789,
+ "max-ref-age-ms":987654321})"_json;
+
+ TestJsonConversion(ref, expected_json);
+}
+
+TEST(JsonInternalTest, SnapshotRefTag) {
+ SnapshotRef ref(9876543210, SnapshotRef::Tag{.max_ref_age_ms = 54321});
+
+ // Create a JSON object with the expected values
+ nlohmann::json expected_json =
+ R"({"snapshot-id":9876543210,
+ "type":"tag",
+ "max-ref-age-ms":54321})"_json;
+
+ TestJsonConversion(ref, expected_json);
+}
+
+TEST(JsonInternalTest, Snapshot) {
+ std::unordered_map<std::string, std::string> summary = {
+ {SnapshotSummaryFields::kOperation, DataOperation::kAppend},
+ {SnapshotSummaryFields::kAddedDataFiles, "50"}};
+
+ Snapshot snapshot{.snapshot_id = 1234567890,
+ .parent_snapshot_id = 9876543210,
+ .sequence_number = 99,
+ .timestamp_ms = 1234567890123,
+ .manifest_list = "/path/to/manifest_list",
+ .summary = summary,
+ .schema_id = 42};
+
+ // Create a JSON object with the expected values
+ nlohmann::json expected_json =
+ R"({"snapshot-id":1234567890,
+ "parent-snapshot-id":9876543210,
+ "sequence-number":99,
+ "timestamp-ms":1234567890123,
+ "manifest-list":"/path/to/manifest_list",
+ "summary":{
+ "operation":"append",
+ "added-data-files":"50"
+ },
+ "schema-id":42})"_json;
+
+ TestJsonConversion(snapshot, expected_json);
+}
+
+TEST(JsonInternalTest, SnapshotFromJsonWithInvalidSummary) {
+ nlohmann::json invalid_json =
+ R"({"snapshot-id":1234567890,
+ "parent-snapshot-id":9876543210,
+ "sequence-number":99,
+ "timestamp-ms":1234567890123,
+ "manifest-list":"/path/to/manifest_list",
+ "summary":{
+ "invalid-field":"value"
+ },
+ "schema-id":42})"_json;
+ // malformed summary field
+
+ auto result = SnapshotFromJson(invalid_json);
+ ASSERT_FALSE(result.has_value());
+
+ EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+ EXPECT_THAT(result, HasErrorMessage("Invalid snapshot summary field"));
+}
+
+TEST(JsonInternalTest, SnapshotFromJsonSummaryWithNoOperation) {
+ nlohmann::json snapshot_json =
+ R"({"snapshot-id":1234567890,
+ "parent-snapshot-id":9876543210,
+ "sequence-number":99,
+ "timestamp-ms":1234567890123,
+ "manifest-list":"/path/to/manifest_list",
+ "summary":{
+ "added-data-files":"50"
+ },
+ "schema-id":42})"_json;
+
+ auto result = SnapshotFromJson(snapshot_json);
+ ASSERT_TRUE(result.has_value());
+
+ ASSERT_EQ(result.value()->operation(), DataOperation::kOverwrite);
+}
+
} // namespace iceberg