This is an automated email from the ASF dual-hosted git repository.
gangwu 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 a7851190 feat(rest): implement list table and update table (#484)
a7851190 is described below
commit a78511904ae77f67f841ee1d98285daded608b0a
Author: Feiyang Li <[email protected]>
AuthorDate: Tue Jan 6 16:34:01 2026 +0800
feat(rest): implement list table and update table (#484)
---
cmake_modules/IcebergBuildUtils.cmake | 8 +
src/iceberg/catalog/rest/http_client.cc | 34 ++-
src/iceberg/catalog/rest/http_client.h | 3 +-
src/iceberg/catalog/rest/json_internal.cc | 75 +++++
src/iceberg/catalog/rest/json_internal.h | 2 +
src/iceberg/catalog/rest/rest_catalog.cc | 98 ++++++-
src/iceberg/catalog/rest/types.cc | 47 ++++
src/iceberg/catalog/rest/types.h | 48 +++-
src/iceberg/json_internal.cc | 419 +++++++++++++++++++++++++++-
src/iceberg/json_internal.h | 68 +++--
src/iceberg/table_requirement.h | 113 ++++++++
src/iceberg/table_update.cc | 236 ++++++++++++++++
src/iceberg/table_update.h | 84 ++++++
src/iceberg/test/CMakeLists.txt | 7 +
src/iceberg/test/json_internal_test.cc | 338 ++++++++++++++++++++++
src/iceberg/test/rest_catalog_test.cc | 155 ++++++++++
src/iceberg/test/rest_json_internal_test.cc | 202 ++++++++++++++
17 files changed, 1881 insertions(+), 56 deletions(-)
diff --git a/cmake_modules/IcebergBuildUtils.cmake
b/cmake_modules/IcebergBuildUtils.cmake
index 99f57d92..74b02970 100644
--- a/cmake_modules/IcebergBuildUtils.cmake
+++ b/cmake_modules/IcebergBuildUtils.cmake
@@ -157,6 +157,10 @@ function(add_iceberg_lib LIB_NAME)
hidden
VISIBILITY_INLINES_HIDDEN 1)
+ if(MSVC_TOOLCHAIN)
+ target_compile_options(${LIB_NAME}_shared PRIVATE /bigobj)
+ endif()
+
install(TARGETS ${LIB_NAME}_shared
EXPORT iceberg_targets
ARCHIVE DESTINATION ${INSTALL_ARCHIVE_DIR}
@@ -220,6 +224,10 @@ function(add_iceberg_lib LIB_NAME)
target_compile_definitions(${LIB_NAME}_static PUBLIC
${VISIBILITY_NAME}_STATIC)
endif()
+ if(MSVC_TOOLCHAIN)
+ target_compile_options(${LIB_NAME}_static PRIVATE /bigobj)
+ endif()
+
install(TARGETS ${LIB_NAME}_static
EXPORT iceberg_targets
ARCHIVE DESTINATION ${INSTALL_ARCHIVE_DIR}
diff --git a/src/iceberg/catalog/rest/http_client.cc
b/src/iceberg/catalog/rest/http_client.cc
index 7804ebd5..84d458b9 100644
--- a/src/iceberg/catalog/rest/http_client.cc
+++ b/src/iceberg/catalog/rest/http_client.cc
@@ -135,11 +135,33 @@ Status HandleFailureResponse(const cpr::Response&
response,
} // namespace
void HttpClient::PrepareSession(
- const std::string& path, const std::unordered_map<std::string,
std::string>& params,
+ const std::string& path, HttpMethod method,
+ const std::unordered_map<std::string, std::string>& params,
const std::unordered_map<std::string, std::string>& headers) {
session_->SetUrl(cpr::Url{path});
session_->SetParameters(GetParameters(params));
session_->RemoveContent();
+ // clear lingering POST mode state from prior requests. CURLOPT_POST is
implicitly set
+ // to 1 by POST requests, and this state is not reset by RemoveContent(), so
we must
+ // manually enforce HTTP GET to clear it.
+ curl_easy_setopt(session_->GetCurlHolder()->handle, CURLOPT_HTTPGET, 1L);
+ switch (method) {
+ case HttpMethod::kGet:
+ session_->PrepareGet();
+ break;
+ case HttpMethod::kPost:
+ session_->PreparePost();
+ break;
+ case HttpMethod::kPut:
+ session_->PreparePut();
+ break;
+ case HttpMethod::kDelete:
+ session_->PrepareDelete();
+ break;
+ case HttpMethod::kHead:
+ session_->PrepareHead();
+ break;
+ }
auto final_headers = MergeHeaders(default_headers_, headers);
session_->SetHeader(final_headers);
}
@@ -163,7 +185,7 @@ Result<HttpResponse> HttpClient::Get(
cpr::Response response;
{
std::lock_guard guard(session_mutex_);
- PrepareSession(path, params, headers);
+ PrepareSession(path, HttpMethod::kGet, params, headers);
response = session_->Get();
}
@@ -180,7 +202,7 @@ Result<HttpResponse> HttpClient::Post(
cpr::Response response;
{
std::lock_guard guard(session_mutex_);
- PrepareSession(path, /*params=*/{}, headers);
+ PrepareSession(path, HttpMethod::kPost, /*params=*/{}, headers);
session_->SetBody(cpr::Body{body});
response = session_->Post();
}
@@ -205,7 +227,7 @@ Result<HttpResponse> HttpClient::PostForm(
auto form_headers = headers;
form_headers[kHeaderContentType] = kMimeTypeFormUrlEncoded;
- PrepareSession(path, /*params=*/{}, form_headers);
+ PrepareSession(path, HttpMethod::kPost, /*params=*/{}, form_headers);
std::vector<cpr::Pair> pair_list;
pair_list.reserve(form_data.size());
for (const auto& [key, val] : form_data) {
@@ -228,7 +250,7 @@ Result<HttpResponse> HttpClient::Head(
cpr::Response response;
{
std::lock_guard guard(session_mutex_);
- PrepareSession(path, /*params=*/{}, headers);
+ PrepareSession(path, HttpMethod::kHead, /*params=*/{}, headers);
response = session_->Head();
}
@@ -245,7 +267,7 @@ Result<HttpResponse> HttpClient::Delete(
cpr::Response response;
{
std::lock_guard guard(session_mutex_);
- PrepareSession(path, params, headers);
+ PrepareSession(path, HttpMethod::kDelete, params, headers);
response = session_->Delete();
}
diff --git a/src/iceberg/catalog/rest/http_client.h
b/src/iceberg/catalog/rest/http_client.h
index a1401b63..84f8e590 100644
--- a/src/iceberg/catalog/rest/http_client.h
+++ b/src/iceberg/catalog/rest/http_client.h
@@ -25,6 +25,7 @@
#include <string>
#include <unordered_map>
+#include "iceberg/catalog/rest/endpoint.h"
#include "iceberg/catalog/rest/iceberg_rest_export.h"
#include "iceberg/catalog/rest/type_fwd.h"
#include "iceberg/result.h"
@@ -109,7 +110,7 @@ class ICEBERG_REST_EXPORT HttpClient {
const ErrorHandler& error_handler);
private:
- void PrepareSession(const std::string& path,
+ void PrepareSession(const std::string& path, HttpMethod method,
const std::unordered_map<std::string, std::string>&
params,
const std::unordered_map<std::string, std::string>&
headers);
diff --git a/src/iceberg/catalog/rest/json_internal.cc
b/src/iceberg/catalog/rest/json_internal.cc
index b6bb970e..b9c4caca 100644
--- a/src/iceberg/catalog/rest/json_internal.cc
+++ b/src/iceberg/catalog/rest/json_internal.cc
@@ -31,6 +31,8 @@
#include "iceberg/partition_spec.h"
#include "iceberg/sort_order.h"
#include "iceberg/table_identifier.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_update.h"
#include "iceberg/util/json_util_internal.h"
#include "iceberg/util/macros.h"
@@ -69,6 +71,8 @@ constexpr std::string_view kType = "type";
constexpr std::string_view kCode = "code";
constexpr std::string_view kStack = "stack";
constexpr std::string_view kError = "error";
+constexpr std::string_view kIdentifier = "identifier";
+constexpr std::string_view kRequirements = "requirements";
} // namespace
@@ -390,6 +394,75 @@ Result<CreateTableRequest>
CreateTableRequestFromJson(const nlohmann::json& json
return request;
}
+// CommitTableRequest serialization
+nlohmann::json ToJson(const CommitTableRequest& request) {
+ nlohmann::json json;
+ if (!request.identifier.name.empty()) {
+ json[kIdentifier] = ToJson(request.identifier);
+ }
+
+ nlohmann::json requirements_json = nlohmann::json::array();
+ for (const auto& req : request.requirements) {
+ requirements_json.push_back(ToJson(*req));
+ }
+ json[kRequirements] = std::move(requirements_json);
+
+ nlohmann::json updates_json = nlohmann::json::array();
+ for (const auto& update : request.updates) {
+ updates_json.push_back(ToJson(*update));
+ }
+ json[kUpdates] = std::move(updates_json);
+
+ return json;
+}
+
+Result<CommitTableRequest> CommitTableRequestFromJson(const nlohmann::json&
json) {
+ CommitTableRequest request;
+ if (json.contains(kIdentifier)) {
+ ICEBERG_ASSIGN_OR_RAISE(auto identifier_json,
+ GetJsonValue<nlohmann::json>(json, kIdentifier));
+ ICEBERG_ASSIGN_OR_RAISE(request.identifier,
TableIdentifierFromJson(identifier_json));
+ }
+
+ ICEBERG_ASSIGN_OR_RAISE(auto requirements_json,
+ GetJsonValue<nlohmann::json>(json, kRequirements));
+ for (const auto& req_json : requirements_json) {
+ ICEBERG_ASSIGN_OR_RAISE(auto requirement,
TableRequirementFromJson(req_json));
+ request.requirements.push_back(std::move(requirement));
+ }
+
+ ICEBERG_ASSIGN_OR_RAISE(auto updates_json,
+ GetJsonValue<nlohmann::json>(json, kUpdates));
+ for (const auto& update_json : updates_json) {
+ ICEBERG_ASSIGN_OR_RAISE(auto update, TableUpdateFromJson(update_json));
+ request.updates.push_back(std::move(update));
+ }
+
+ ICEBERG_RETURN_UNEXPECTED(request.Validate());
+ return request;
+}
+
+// CommitTableResponse serialization
+nlohmann::json ToJson(const CommitTableResponse& response) {
+ nlohmann::json json;
+ json[kMetadataLocation] = response.metadata_location;
+ if (response.metadata) {
+ json[kMetadata] = ToJson(*response.metadata);
+ }
+ return json;
+}
+
+Result<CommitTableResponse> CommitTableResponseFromJson(const nlohmann::json&
json) {
+ CommitTableResponse response;
+ ICEBERG_ASSIGN_OR_RAISE(response.metadata_location,
+ GetJsonValue<std::string>(json, kMetadataLocation));
+ ICEBERG_ASSIGN_OR_RAISE(auto metadata_json,
+ GetJsonValue<nlohmann::json>(json, kMetadata));
+ ICEBERG_ASSIGN_OR_RAISE(response.metadata,
TableMetadataFromJson(metadata_json));
+ ICEBERG_RETURN_UNEXPECTED(response.Validate());
+ return response;
+}
+
#define ICEBERG_DEFINE_FROM_JSON(Model) \
template <> \
Result<Model> FromJson<Model>(const nlohmann::json& json) { \
@@ -409,5 +482,7 @@ ICEBERG_DEFINE_FROM_JSON(LoadTableResult)
ICEBERG_DEFINE_FROM_JSON(RegisterTableRequest)
ICEBERG_DEFINE_FROM_JSON(RenameTableRequest)
ICEBERG_DEFINE_FROM_JSON(CreateTableRequest)
+ICEBERG_DEFINE_FROM_JSON(CommitTableRequest)
+ICEBERG_DEFINE_FROM_JSON(CommitTableResponse)
} // namespace iceberg::rest
diff --git a/src/iceberg/catalog/rest/json_internal.h
b/src/iceberg/catalog/rest/json_internal.h
index e2a88b4c..081a0205 100644
--- a/src/iceberg/catalog/rest/json_internal.h
+++ b/src/iceberg/catalog/rest/json_internal.h
@@ -56,6 +56,8 @@ ICEBERG_DECLARE_JSON_SERDE(LoadTableResult)
ICEBERG_DECLARE_JSON_SERDE(RegisterTableRequest)
ICEBERG_DECLARE_JSON_SERDE(RenameTableRequest)
ICEBERG_DECLARE_JSON_SERDE(CreateTableRequest)
+ICEBERG_DECLARE_JSON_SERDE(CommitTableRequest)
+ICEBERG_DECLARE_JSON_SERDE(CommitTableResponse)
#undef ICEBERG_DECLARE_JSON_SERDE
diff --git a/src/iceberg/catalog/rest/rest_catalog.cc
b/src/iceberg/catalog/rest/rest_catalog.cc
index d75da640..a799d69a 100644
--- a/src/iceberg/catalog/rest/rest_catalog.cc
+++ b/src/iceberg/catalog/rest/rest_catalog.cc
@@ -42,6 +42,8 @@
#include "iceberg/schema.h"
#include "iceberg/sort_order.h"
#include "iceberg/table.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_update.h"
#include "iceberg/util/macros.h"
namespace iceberg::rest {
@@ -177,7 +179,7 @@ Result<std::vector<Namespace>>
RestCatalog::ListNamespaces(const Namespace& ns)
if (list_response.next_page_token.empty()) {
return result;
}
- next_token = list_response.next_page_token;
+ next_token = std::move(list_response.next_page_token);
}
return result;
}
@@ -246,9 +248,30 @@ Status RestCatalog::UpdateNamespaceProperties(
return {};
}
-Result<std::vector<TableIdentifier>> RestCatalog::ListTables(
- [[maybe_unused]] const Namespace& ns) const {
- return NotImplemented("Not implemented");
+Result<std::vector<TableIdentifier>> RestCatalog::ListTables(const Namespace&
ns) const {
+ ICEBERG_ENDPOINT_CHECK(supported_endpoints_, Endpoint::ListTables());
+
+ ICEBERG_ASSIGN_OR_RAISE(auto path, paths_->Tables(ns));
+ std::vector<TableIdentifier> result;
+ std::string next_token;
+ while (true) {
+ std::unordered_map<std::string, std::string> params;
+ if (!next_token.empty()) {
+ params[kQueryParamPageToken] = next_token;
+ }
+ ICEBERG_ASSIGN_OR_RAISE(
+ const auto response,
+ client_->Get(path, params, /*headers=*/{},
*TableErrorHandler::Instance()));
+ ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
+ ICEBERG_ASSIGN_OR_RAISE(auto list_response,
ListTablesResponseFromJson(json));
+ result.insert(result.end(), list_response.identifiers.begin(),
+ list_response.identifiers.end());
+ if (list_response.next_page_token.empty()) {
+ return result;
+ }
+ next_token = std::move(list_response.next_page_token);
+ }
+ return result;
}
Result<std::shared_ptr<Table>> RestCatalog::CreateTable(
@@ -282,10 +305,33 @@ Result<std::shared_ptr<Table>> RestCatalog::CreateTable(
}
Result<std::shared_ptr<Table>> RestCatalog::UpdateTable(
- [[maybe_unused]] const TableIdentifier& identifier,
- [[maybe_unused]] const std::vector<std::unique_ptr<TableRequirement>>&
requirements,
- [[maybe_unused]] const std::vector<std::unique_ptr<TableUpdate>>& updates)
{
- return NotImplemented("Not implemented");
+ const TableIdentifier& identifier,
+ const std::vector<std::unique_ptr<TableRequirement>>& requirements,
+ const std::vector<std::unique_ptr<TableUpdate>>& updates) {
+ ICEBERG_ENDPOINT_CHECK(supported_endpoints_, Endpoint::UpdateTable());
+ ICEBERG_ASSIGN_OR_RAISE(auto path, paths_->Table(identifier));
+
+ CommitTableRequest request{.identifier = identifier};
+ request.requirements.reserve(requirements.size());
+ for (const auto& req : requirements) {
+ request.requirements.push_back(req->Clone());
+ }
+ request.updates.reserve(updates.size());
+ for (const auto& update : updates) {
+ request.updates.push_back(update->Clone());
+ }
+
+ ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
+ ICEBERG_ASSIGN_OR_RAISE(
+ const auto response,
+ client_->Post(path, json_request, /*headers=*/{},
*TableErrorHandler::Instance()));
+
+ ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
+ ICEBERG_ASSIGN_OR_RAISE(auto commit_response,
CommitTableResponseFromJson(json));
+
+ return Table::Make(identifier, std::move(commit_response.metadata),
+ std::move(commit_response.metadata_location), file_io_,
+ shared_from_this());
}
Result<std::shared_ptr<Transaction>> RestCatalog::StageCreateTable(
@@ -323,9 +369,17 @@ Result<bool> RestCatalog::TableExists(const
TableIdentifier& identifier) const {
client_->Head(path, /*headers=*/{}, *TableErrorHandler::Instance()));
}
-Status RestCatalog::RenameTable([[maybe_unused]] const TableIdentifier& from,
- [[maybe_unused]] const TableIdentifier& to) {
- return NotImplemented("Not implemented");
+Status RestCatalog::RenameTable(const TableIdentifier& from, const
TableIdentifier& to) {
+ ICEBERG_ENDPOINT_CHECK(supported_endpoints_, Endpoint::RenameTable());
+ ICEBERG_ASSIGN_OR_RAISE(auto path, paths_->Rename());
+
+ RenameTableRequest request{.source = from, .destination = to};
+ ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
+ ICEBERG_ASSIGN_OR_RAISE(
+ const auto response,
+ client_->Post(path, json_request, /*headers=*/{},
*TableErrorHandler::Instance()));
+
+ return {};
}
Result<std::string> RestCatalog::LoadTableInternal(
@@ -352,9 +406,25 @@ Result<std::shared_ptr<Table>>
RestCatalog::LoadTable(const TableIdentifier& ide
}
Result<std::shared_ptr<Table>> RestCatalog::RegisterTable(
- [[maybe_unused]] const TableIdentifier& identifier,
- [[maybe_unused]] const std::string& metadata_file_location) {
- return NotImplemented("Not implemented");
+ const TableIdentifier& identifier, const std::string&
metadata_file_location) {
+ ICEBERG_ENDPOINT_CHECK(supported_endpoints_, Endpoint::RegisterTable());
+ ICEBERG_ASSIGN_OR_RAISE(auto path, paths_->Register(identifier.ns));
+
+ RegisterTableRequest request{
+ .name = identifier.name,
+ .metadata_location = metadata_file_location,
+ };
+
+ ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
+ ICEBERG_ASSIGN_OR_RAISE(
+ const auto response,
+ client_->Post(path, json_request, /*headers=*/{},
*TableErrorHandler::Instance()));
+
+ ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
+ ICEBERG_ASSIGN_OR_RAISE(auto load_result, LoadTableResultFromJson(json));
+ return Table::Make(identifier, std::move(load_result.metadata),
+ std::move(load_result.metadata_location), file_io_,
+ shared_from_this());
}
} // namespace iceberg::rest
diff --git a/src/iceberg/catalog/rest/types.cc
b/src/iceberg/catalog/rest/types.cc
index 5c23e47b..3416bfe3 100644
--- a/src/iceberg/catalog/rest/types.cc
+++ b/src/iceberg/catalog/rest/types.cc
@@ -23,6 +23,8 @@
#include "iceberg/schema.h"
#include "iceberg/sort_order.h"
#include "iceberg/table_metadata.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_update.h"
namespace iceberg::rest {
@@ -69,4 +71,49 @@ bool LoadTableResult::operator==(const LoadTableResult&
other) const {
return true;
}
+bool CommitTableRequest::operator==(const CommitTableRequest& other) const {
+ if (identifier != other.identifier) {
+ return false;
+ }
+ if (requirements.size() != other.requirements.size()) {
+ return false;
+ }
+ if (updates.size() != other.updates.size()) {
+ return false;
+ }
+
+ for (size_t i = 0; i < requirements.size(); ++i) {
+ if (!requirements[i] != !other.requirements[i]) {
+ return false;
+ }
+ if (requirements[i] && !requirements[i]->Equals(*other.requirements[i])) {
+ return false;
+ }
+ }
+
+ for (size_t i = 0; i < updates.size(); ++i) {
+ if (!updates[i] != !other.updates[i]) {
+ return false;
+ }
+ if (updates[i] && !updates[i]->Equals(*other.updates[i])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool CommitTableResponse::operator==(const CommitTableResponse& other) const {
+ if (metadata_location != other.metadata_location) {
+ return false;
+ }
+ if (!metadata != !other.metadata) {
+ return false;
+ }
+ if (metadata && *metadata != *other.metadata) {
+ return false;
+ }
+ return true;
+}
+
} // namespace iceberg::rest
diff --git a/src/iceberg/catalog/rest/types.h b/src/iceberg/catalog/rest/types.h
index 01fe330d..93e7048a 100644
--- a/src/iceberg/catalog/rest/types.h
+++ b/src/iceberg/catalog/rest/types.h
@@ -59,11 +59,12 @@ struct ICEBERG_REST_EXPORT ErrorResponse {
/// \brief Validates the ErrorResponse.
Status Validate() const {
if (message.empty() || type.empty()) {
- return Invalid("Invalid error response: missing required fields");
+ return ValidationFailed("Invalid error response: missing required
fields");
}
if (code < 400 || code > 600) {
- return Invalid("Invalid error response: code {} is out of range [400,
600]", code);
+ return ValidationFailed(
+ "Invalid error response: code {} is out of range [400, 600]", code);
}
// stack is optional, no validation needed
@@ -93,7 +94,7 @@ struct ICEBERG_REST_EXPORT UpdateNamespacePropertiesRequest {
Status Validate() const {
for (const auto& key : removals) {
if (updates.contains(key)) {
- return Invalid("Duplicate key to update and remove: {}", key);
+ return ValidationFailed("Duplicate key to update and remove: {}", key);
}
}
return {};
@@ -111,11 +112,11 @@ struct ICEBERG_REST_EXPORT RegisterTableRequest {
/// \brief Validates the RegisterTableRequest.
Status Validate() const {
if (name.empty()) {
- return Invalid("Missing table name");
+ return ValidationFailed("Missing table name");
}
if (metadata_location.empty()) {
- return Invalid("Empty metadata location");
+ return ValidationFailed("Empty metadata location");
}
return {};
@@ -152,10 +153,10 @@ struct ICEBERG_REST_EXPORT CreateTableRequest {
/// \brief Validates the CreateTableRequest.
Status Validate() const {
if (name.empty()) {
- return Invalid("Missing table name");
+ return ValidationFailed("Missing table name");
}
if (!schema) {
- return Invalid("Missing schema");
+ return ValidationFailed("Missing schema");
}
return {};
}
@@ -176,7 +177,7 @@ struct ICEBERG_REST_EXPORT LoadTableResult {
/// \brief Validates the LoadTableResult.
Status Validate() const {
if (!metadata) {
- return Invalid("Invalid metadata: null");
+ return ValidationFailed("Invalid metadata: null");
}
return {};
}
@@ -246,4 +247,35 @@ struct ICEBERG_REST_EXPORT ListTablesResponse {
bool operator==(const ListTablesResponse&) const = default;
};
+/// \brief Request to commit changes to a table.
+struct ICEBERG_REST_EXPORT CommitTableRequest {
+ TableIdentifier identifier;
+ std::vector<std::shared_ptr<TableRequirement>> requirements; // required
+ std::vector<std::shared_ptr<TableUpdate>> updates; // required
+
+ /// \brief Validates the CommitTableRequest.
+ Status Validate() const { return {}; }
+
+ bool operator==(const CommitTableRequest& other) const;
+};
+
+/// \brief Response from committing changes to a table.
+struct ICEBERG_REST_EXPORT CommitTableResponse {
+ std::string metadata_location; // required
+ std::shared_ptr<TableMetadata> metadata; // required
+
+ /// \brief Validates the CommitTableResponse.
+ Status Validate() const {
+ if (metadata_location.empty()) {
+ return ValidationFailed("Invalid metadata location: empty");
+ }
+ if (!metadata) {
+ return ValidationFailed("Invalid metadata: null");
+ }
+ return {};
+ }
+
+ bool operator==(const CommitTableResponse& other) const;
+};
+
} // namespace iceberg::rest
diff --git a/src/iceberg/json_internal.cc b/src/iceberg/json_internal.cc
index a52f418e..30b32144 100644
--- a/src/iceberg/json_internal.cc
+++ b/src/iceberg/json_internal.cc
@@ -40,6 +40,8 @@
#include "iceberg/table_identifier.h"
#include "iceberg/table_metadata.h"
#include "iceberg/table_properties.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_update.h"
#include "iceberg/transform.h"
#include "iceberg/type.h"
#include "iceberg/util/checked_cast.h"
@@ -170,6 +172,54 @@ constexpr std::string_view kFileSizeInBytes =
"file-size-in-bytes";
constexpr std::string_view kFileFooterSizeInBytes =
"file-footer-size-in-bytes";
constexpr std::string_view kBlobMetadata = "blob-metadata";
+// TableUpdate action constants
+constexpr std::string_view kAction = "action";
+constexpr std::string_view kActionAssignUUID = "assign-uuid";
+constexpr std::string_view kActionUpgradeFormatVersion =
"upgrade-format-version";
+constexpr std::string_view kActionAddSchema = "add-schema";
+constexpr std::string_view kActionSetCurrentSchema = "set-current-schema";
+constexpr std::string_view kActionAddPartitionSpec = "add-spec";
+constexpr std::string_view kActionSetDefaultPartitionSpec = "set-default-spec";
+constexpr std::string_view kActionRemovePartitionSpecs =
"remove-partition-specs";
+constexpr std::string_view kActionRemoveSchemas = "remove-schemas";
+constexpr std::string_view kActionAddSortOrder = "add-sort-order";
+constexpr std::string_view kActionSetDefaultSortOrder =
"set-default-sort-order";
+constexpr std::string_view kActionAddSnapshot = "add-snapshot";
+constexpr std::string_view kActionRemoveSnapshots = "remove-snapshots";
+constexpr std::string_view kActionRemoveSnapshotRef = "remove-snapshot-ref";
+constexpr std::string_view kActionSetSnapshotRef = "set-snapshot-ref";
+constexpr std::string_view kActionSetProperties = "set-properties";
+constexpr std::string_view kActionRemoveProperties = "remove-properties";
+constexpr std::string_view kActionSetLocation = "set-location";
+
+// TableUpdate field constants
+constexpr std::string_view kUUID = "uuid";
+constexpr std::string_view kSpec = "spec";
+constexpr std::string_view kSpecIds = "spec-ids";
+constexpr std::string_view kSchemaIds = "schema-ids";
+constexpr std::string_view kSortOrder = "sort-order";
+constexpr std::string_view kSortOrderId = "sort-order-id";
+constexpr std::string_view kSnapshot = "snapshot";
+constexpr std::string_view kSnapshotIds = "snapshot-ids";
+constexpr std::string_view kRefName = "ref-name";
+constexpr std::string_view kUpdates = "updates";
+constexpr std::string_view kRemovals = "removals";
+
+// TableRequirement type constants
+constexpr std::string_view kRequirementAssertDoesNotExist = "assert-create";
+constexpr std::string_view kRequirementAssertUUID = "assert-table-uuid";
+constexpr std::string_view kRequirementAssertRefSnapshotID =
"assert-ref-snapshot-id";
+constexpr std::string_view kRequirementAssertLastAssignedFieldId =
+ "assert-last-assigned-field-id";
+constexpr std::string_view kRequirementAssertCurrentSchemaID =
"assert-current-schema-id";
+constexpr std::string_view kRequirementAssertLastAssignedPartitionId =
+ "assert-last-assigned-partition-id";
+constexpr std::string_view kRequirementAssertDefaultSpecID =
"assert-default-spec-id";
+constexpr std::string_view kRequirementAssertDefaultSortOrderID =
+ "assert-default-sort-order-id";
+constexpr std::string_view kLastAssignedFieldId = "last-assigned-field-id";
+constexpr std::string_view kLastAssignedPartitionId =
"last-assigned-partition-id";
+
} // namespace
nlohmann::json ToJson(const SortField& sort_field) {
@@ -233,7 +283,7 @@ nlohmann::json ToJson(const SchemaField& field) {
nlohmann::json ToJson(const Type& type) {
switch (type.type_id()) {
case TypeId::kStruct: {
- const auto& struct_type = static_cast<const StructType&>(type);
+ const auto& struct_type = internal::checked_cast<const
StructType&>(type);
nlohmann::json json;
json[kType] = kStruct;
nlohmann::json fields_json = nlohmann::json::array();
@@ -245,7 +295,7 @@ nlohmann::json ToJson(const Type& type) {
return json;
}
case TypeId::kList: {
- const auto& list_type = static_cast<const ListType&>(type);
+ const auto& list_type = internal::checked_cast<const ListType&>(type);
nlohmann::json json;
json[kType] = kList;
@@ -256,7 +306,7 @@ nlohmann::json ToJson(const Type& type) {
return json;
}
case TypeId::kMap: {
- const auto& map_type = static_cast<const MapType&>(type);
+ const auto& map_type = internal::checked_cast<const MapType&>(type);
nlohmann::json json;
json[std::string(kType)] = kMap;
@@ -281,7 +331,7 @@ nlohmann::json ToJson(const Type& type) {
case TypeId::kDouble:
return "double";
case TypeId::kDecimal: {
- const auto& decimal_type = static_cast<const DecimalType&>(type);
+ const auto& decimal_type = internal::checked_cast<const
DecimalType&>(type);
return std::format("decimal({},{})", decimal_type.precision(),
decimal_type.scale());
}
@@ -298,7 +348,7 @@ nlohmann::json ToJson(const Type& type) {
case TypeId::kBinary:
return "binary";
case TypeId::kFixed: {
- const auto& fixed_type = static_cast<const FixedType&>(type);
+ const auto& fixed_type = internal::checked_cast<const FixedType&>(type);
return std::format("fixed[{}]", fixed_type.length());
}
case TypeId::kUuid:
@@ -308,7 +358,7 @@ nlohmann::json ToJson(const Type& type) {
}
nlohmann::json ToJson(const Schema& schema) {
- nlohmann::json json = ToJson(static_cast<const Type&>(schema));
+ nlohmann::json json = ToJson(internal::checked_cast<const Type&>(schema));
json[kSchemaId] = schema.schema_id();
if (!schema.IdentifierFieldIds().empty()) {
json[kIdentifierFieldIds] = schema.IdentifierFieldIds();
@@ -1214,4 +1264,361 @@ Result<Namespace> NamespaceFromJson(const
nlohmann::json& json) {
return ns;
}
+nlohmann::json ToJson(const TableUpdate& update) {
+ nlohmann::json json;
+ switch (update.kind()) {
+ case TableUpdate::Kind::kAssignUUID: {
+ const auto& u = internal::checked_cast<const table::AssignUUID&>(update);
+ json[kAction] = kActionAssignUUID;
+ json[kUUID] = u.uuid();
+ break;
+ }
+ case TableUpdate::Kind::kUpgradeFormatVersion: {
+ const auto& u = internal::checked_cast<const
table::UpgradeFormatVersion&>(update);
+ json[kAction] = kActionUpgradeFormatVersion;
+ json[kFormatVersion] = u.format_version();
+ break;
+ }
+ case TableUpdate::Kind::kAddSchema: {
+ const auto& u = internal::checked_cast<const table::AddSchema&>(update);
+ json[kAction] = kActionAddSchema;
+ if (u.schema()) {
+ json[kSchema] = ToJson(*u.schema());
+ } else {
+ json[kSchema] = nlohmann::json::value_t::null;
+ }
+ json[kLastColumnId] = u.last_column_id();
+ break;
+ }
+ case TableUpdate::Kind::kSetCurrentSchema: {
+ const auto& u = internal::checked_cast<const
table::SetCurrentSchema&>(update);
+ json[kAction] = kActionSetCurrentSchema;
+ json[kSchemaId] = u.schema_id();
+ break;
+ }
+ case TableUpdate::Kind::kAddPartitionSpec: {
+ const auto& u = internal::checked_cast<const
table::AddPartitionSpec&>(update);
+ json[kAction] = kActionAddPartitionSpec;
+ if (u.spec()) {
+ json[kSpec] = ToJson(*u.spec());
+ } else {
+ json[kSpec] = nlohmann::json::value_t::null;
+ }
+ break;
+ }
+ case TableUpdate::Kind::kSetDefaultPartitionSpec: {
+ const auto& u =
+ internal::checked_cast<const
table::SetDefaultPartitionSpec&>(update);
+ json[kAction] = kActionSetDefaultPartitionSpec;
+ json[kSpecId] = u.spec_id();
+ break;
+ }
+ case TableUpdate::Kind::kRemovePartitionSpecs: {
+ const auto& u = internal::checked_cast<const
table::RemovePartitionSpecs&>(update);
+ json[kAction] = kActionRemovePartitionSpecs;
+ json[kSpecIds] = u.spec_ids();
+ break;
+ }
+ case TableUpdate::Kind::kRemoveSchemas: {
+ const auto& u = internal::checked_cast<const
table::RemoveSchemas&>(update);
+ json[kAction] = kActionRemoveSchemas;
+ json[kSchemaIds] = u.schema_ids();
+ break;
+ }
+ case TableUpdate::Kind::kAddSortOrder: {
+ const auto& u = internal::checked_cast<const
table::AddSortOrder&>(update);
+ json[kAction] = kActionAddSortOrder;
+ if (u.sort_order()) {
+ json[kSortOrder] = ToJson(*u.sort_order());
+ } else {
+ json[kSortOrder] = nlohmann::json::value_t::null;
+ }
+ break;
+ }
+ case TableUpdate::Kind::kSetDefaultSortOrder: {
+ const auto& u = internal::checked_cast<const
table::SetDefaultSortOrder&>(update);
+ json[kAction] = kActionSetDefaultSortOrder;
+ json[kSortOrderId] = u.sort_order_id();
+ break;
+ }
+ case TableUpdate::Kind::kAddSnapshot: {
+ const auto& u = internal::checked_cast<const
table::AddSnapshot&>(update);
+ json[kAction] = kActionAddSnapshot;
+ if (u.snapshot()) {
+ json[kSnapshot] = ToJson(*u.snapshot());
+ } else {
+ json[kSnapshot] = nlohmann::json::value_t::null;
+ }
+ break;
+ }
+ case TableUpdate::Kind::kRemoveSnapshots: {
+ const auto& u = internal::checked_cast<const
table::RemoveSnapshots&>(update);
+ json[kAction] = kActionRemoveSnapshots;
+ json[kSnapshotIds] = u.snapshot_ids();
+ break;
+ }
+ case TableUpdate::Kind::kRemoveSnapshotRef: {
+ const auto& u = internal::checked_cast<const
table::RemoveSnapshotRef&>(update);
+ json[kAction] = kActionRemoveSnapshotRef;
+ json[kRefName] = u.ref_name();
+ break;
+ }
+ case TableUpdate::Kind::kSetSnapshotRef: {
+ const auto& u = internal::checked_cast<const
table::SetSnapshotRef&>(update);
+ json[kAction] = kActionSetSnapshotRef;
+ json[kRefName] = u.ref_name();
+ json[kSnapshotId] = u.snapshot_id();
+ json[kType] = ToString(u.type());
+ if (u.min_snapshots_to_keep().has_value()) {
+ json[kMinSnapshotsToKeep] = u.min_snapshots_to_keep().value();
+ }
+ if (u.max_snapshot_age_ms().has_value()) {
+ json[kMaxSnapshotAgeMs] = u.max_snapshot_age_ms().value();
+ }
+ if (u.max_ref_age_ms().has_value()) {
+ json[kMaxRefAgeMs] = u.max_ref_age_ms().value();
+ }
+ break;
+ }
+ case TableUpdate::Kind::kSetProperties: {
+ const auto& u = internal::checked_cast<const
table::SetProperties&>(update);
+ json[kAction] = kActionSetProperties;
+ json[kUpdates] = u.updated();
+ break;
+ }
+ case TableUpdate::Kind::kRemoveProperties: {
+ const auto& u = internal::checked_cast<const
table::RemoveProperties&>(update);
+ json[kAction] = kActionRemoveProperties;
+ json[kRemovals] = std::vector<std::string>(u.removed().begin(),
u.removed().end());
+ break;
+ }
+ case TableUpdate::Kind::kSetLocation: {
+ const auto& u = internal::checked_cast<const
table::SetLocation&>(update);
+ json[kAction] = kActionSetLocation;
+ json[kLocation] = u.location();
+ break;
+ }
+ }
+ return json;
+}
+
+nlohmann::json ToJson(const TableRequirement& requirement) {
+ nlohmann::json json;
+ switch (requirement.kind()) {
+ case TableRequirement::Kind::kAssertDoesNotExist:
+ json[kType] = kRequirementAssertDoesNotExist;
+ break;
+ case TableRequirement::Kind::kAssertUUID: {
+ const auto& r = internal::checked_cast<const
table::AssertUUID&>(requirement);
+ json[kType] = kRequirementAssertUUID;
+ json[kUUID] = r.uuid();
+ break;
+ }
+ case TableRequirement::Kind::kAssertRefSnapshotID: {
+ const auto& r =
+ internal::checked_cast<const
table::AssertRefSnapshotID&>(requirement);
+ json[kType] = kRequirementAssertRefSnapshotID;
+ json[kRefName] = r.ref_name();
+ if (r.snapshot_id().has_value()) {
+ json[kSnapshotId] = r.snapshot_id().value();
+ } else {
+ json[kSnapshotId] = nlohmann::json::value_t::null;
+ }
+ break;
+ }
+ case TableRequirement::Kind::kAssertLastAssignedFieldId: {
+ const auto& r =
+ internal::checked_cast<const
table::AssertLastAssignedFieldId&>(requirement);
+ json[kType] = kRequirementAssertLastAssignedFieldId;
+ json[kLastAssignedFieldId] = r.last_assigned_field_id();
+ break;
+ }
+ case TableRequirement::Kind::kAssertCurrentSchemaID: {
+ const auto& r =
+ internal::checked_cast<const
table::AssertCurrentSchemaID&>(requirement);
+ json[kType] = kRequirementAssertCurrentSchemaID;
+ json[kCurrentSchemaId] = r.schema_id();
+ break;
+ }
+ case TableRequirement::Kind::kAssertLastAssignedPartitionId: {
+ const auto& r = internal::checked_cast<const
table::AssertLastAssignedPartitionId&>(
+ requirement);
+ json[kType] = kRequirementAssertLastAssignedPartitionId;
+ json[kLastAssignedPartitionId] = r.last_assigned_partition_id();
+ break;
+ }
+ case TableRequirement::Kind::kAssertDefaultSpecID: {
+ const auto& r =
+ internal::checked_cast<const
table::AssertDefaultSpecID&>(requirement);
+ json[kType] = kRequirementAssertDefaultSpecID;
+ json[kDefaultSpecId] = r.spec_id();
+ break;
+ }
+ case TableRequirement::Kind::kAssertDefaultSortOrderID: {
+ const auto& r =
+ internal::checked_cast<const
table::AssertDefaultSortOrderID&>(requirement);
+ json[kType] = kRequirementAssertDefaultSortOrderID;
+ json[kDefaultSortOrderId] = r.sort_order_id();
+ break;
+ }
+ }
+ return json;
+}
+
+Result<std::unique_ptr<TableUpdate>> TableUpdateFromJson(const nlohmann::json&
json) {
+ ICEBERG_ASSIGN_OR_RAISE(auto action, GetJsonValue<std::string>(json,
kAction));
+
+ if (action == kActionAssignUUID) {
+ ICEBERG_ASSIGN_OR_RAISE(auto uuid, GetJsonValue<std::string>(json, kUUID));
+ return std::make_unique<table::AssignUUID>(std::move(uuid));
+ }
+ if (action == kActionUpgradeFormatVersion) {
+ ICEBERG_ASSIGN_OR_RAISE(auto format_version,
+ GetJsonValue<int8_t>(json, kFormatVersion));
+ return std::make_unique<table::UpgradeFormatVersion>(format_version);
+ }
+ if (action == kActionAddSchema) {
+ ICEBERG_ASSIGN_OR_RAISE(auto schema_json,
+ GetJsonValue<nlohmann::json>(json, kSchema));
+ ICEBERG_ASSIGN_OR_RAISE(auto parsed_schema, SchemaFromJson(schema_json));
+ ICEBERG_ASSIGN_OR_RAISE(auto last_column_id,
+ GetJsonValue<int32_t>(json, kLastColumnId));
+ return std::make_unique<table::AddSchema>(std::move(parsed_schema),
last_column_id);
+ }
+ if (action == kActionSetCurrentSchema) {
+ ICEBERG_ASSIGN_OR_RAISE(auto schema_id, GetJsonValue<int32_t>(json,
kSchemaId));
+ return std::make_unique<table::SetCurrentSchema>(schema_id);
+ }
+ if (action == kActionAddPartitionSpec) {
+ ICEBERG_ASSIGN_OR_RAISE(auto spec_json, GetJsonValue<nlohmann::json>(json,
kSpec));
+ ICEBERG_ASSIGN_OR_RAISE(auto spec_id_opt,
+ GetJsonValueOptional<int32_t>(spec_json, kSpecId));
+ // TODO(Feiyang Li): add fromJson for UnboundPartitionSpec and then use it
here
+ return NotImplemented("FromJson of TableUpdate::AddPartitionSpec is not
implemented");
+ }
+ if (action == kActionSetDefaultPartitionSpec) {
+ ICEBERG_ASSIGN_OR_RAISE(auto spec_id, GetJsonValue<int32_t>(json,
kSpecId));
+ return std::make_unique<table::SetDefaultPartitionSpec>(spec_id);
+ }
+ if (action == kActionRemovePartitionSpecs) {
+ ICEBERG_ASSIGN_OR_RAISE(auto spec_ids,
+ GetJsonValue<std::vector<int32_t>>(json,
kSpecIds));
+ return std::make_unique<table::RemovePartitionSpecs>(std::move(spec_ids));
+ }
+ if (action == kActionRemoveSchemas) {
+ ICEBERG_ASSIGN_OR_RAISE(auto schema_ids_vec,
+ GetJsonValue<std::vector<int32_t>>(json,
kSchemaIds));
+ std::unordered_set<int32_t> schema_ids(schema_ids_vec.begin(),
schema_ids_vec.end());
+ return std::make_unique<table::RemoveSchemas>(std::move(schema_ids));
+ }
+ if (action == kActionAddSortOrder) {
+ ICEBERG_ASSIGN_OR_RAISE(auto sort_order_json,
+ GetJsonValue<nlohmann::json>(json, kSortOrder));
+ // TODO(Feiyang Li): add fromJson for UnboundSortOrder and then use it here
+ return NotImplemented("FromJson of TableUpdate::AddSortOrder is not
implemented");
+ }
+ if (action == kActionSetDefaultSortOrder) {
+ ICEBERG_ASSIGN_OR_RAISE(auto sort_order_id,
+ GetJsonValue<int32_t>(json, kSortOrderId));
+ return std::make_unique<table::SetDefaultSortOrder>(sort_order_id);
+ }
+ if (action == kActionAddSnapshot) {
+ ICEBERG_ASSIGN_OR_RAISE(auto snapshot_json,
+ GetJsonValue<nlohmann::json>(json, kSnapshot));
+ ICEBERG_ASSIGN_OR_RAISE(auto snapshot, SnapshotFromJson(snapshot_json));
+ return std::make_unique<table::AddSnapshot>(std::move(snapshot));
+ }
+ if (action == kActionRemoveSnapshots) {
+ ICEBERG_ASSIGN_OR_RAISE(auto snapshot_ids,
+ GetJsonValue<std::vector<int64_t>>(json,
kSnapshotIds));
+ return std::make_unique<table::RemoveSnapshots>(std::move(snapshot_ids));
+ }
+ if (action == kActionRemoveSnapshotRef) {
+ ICEBERG_ASSIGN_OR_RAISE(auto ref_name, GetJsonValue<std::string>(json,
kRefName));
+ return std::make_unique<table::RemoveSnapshotRef>(std::move(ref_name));
+ }
+ if (action == kActionSetSnapshotRef) {
+ ICEBERG_ASSIGN_OR_RAISE(auto ref_name, GetJsonValue<std::string>(json,
kRefName));
+ 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));
+ ICEBERG_ASSIGN_OR_RAISE(auto min_snapshots,
+ GetJsonValueOptional<int32_t>(json,
kMinSnapshotsToKeep));
+ ICEBERG_ASSIGN_OR_RAISE(auto max_snapshot_age,
+ GetJsonValueOptional<int64_t>(json,
kMaxSnapshotAgeMs));
+ ICEBERG_ASSIGN_OR_RAISE(auto max_ref_age,
+ GetJsonValueOptional<int64_t>(json, kMaxRefAgeMs));
+ return std::make_unique<table::SetSnapshotRef>(std::move(ref_name),
snapshot_id, type,
+ min_snapshots,
max_snapshot_age,
+ max_ref_age);
+ }
+ if (action == kActionSetProperties) {
+ using StringMap = std::unordered_map<std::string, std::string>;
+ ICEBERG_ASSIGN_OR_RAISE(auto updates, GetJsonValue<StringMap>(json,
kUpdates));
+ return std::make_unique<table::SetProperties>(std::move(updates));
+ }
+ if (action == kActionRemoveProperties) {
+ ICEBERG_ASSIGN_OR_RAISE(auto removals_vec,
+ GetJsonValue<std::vector<std::string>>(json,
kRemovals));
+ std::unordered_set<std::string> removals(
+ std::make_move_iterator(removals_vec.begin()),
+ std::make_move_iterator(removals_vec.end()));
+ return std::make_unique<table::RemoveProperties>(std::move(removals));
+ }
+ if (action == kActionSetLocation) {
+ ICEBERG_ASSIGN_OR_RAISE(auto location, GetJsonValue<std::string>(json,
kLocation));
+ return std::make_unique<table::SetLocation>(std::move(location));
+ }
+
+ return JsonParseError("Unknown table update action: {}", action);
+}
+
+Result<std::unique_ptr<TableRequirement>> TableRequirementFromJson(
+ const nlohmann::json& json) {
+ ICEBERG_ASSIGN_OR_RAISE(auto type, GetJsonValue<std::string>(json, kType));
+
+ if (type == kRequirementAssertDoesNotExist) {
+ return std::make_unique<table::AssertDoesNotExist>();
+ }
+ if (type == kRequirementAssertUUID) {
+ ICEBERG_ASSIGN_OR_RAISE(auto uuid, GetJsonValue<std::string>(json, kUUID));
+ return std::make_unique<table::AssertUUID>(std::move(uuid));
+ }
+ if (type == kRequirementAssertRefSnapshotID) {
+ ICEBERG_ASSIGN_OR_RAISE(auto ref_name, GetJsonValue<std::string>(json,
kRefName));
+ ICEBERG_ASSIGN_OR_RAISE(auto snapshot_id_opt,
+ GetJsonValueOptional<int64_t>(json, kSnapshotId));
+ return std::make_unique<table::AssertRefSnapshotID>(std::move(ref_name),
+ snapshot_id_opt);
+ }
+ if (type == kRequirementAssertLastAssignedFieldId) {
+ ICEBERG_ASSIGN_OR_RAISE(auto last_assigned_field_id,
+ GetJsonValue<int32_t>(json, kLastAssignedFieldId));
+ return
std::make_unique<table::AssertLastAssignedFieldId>(last_assigned_field_id);
+ }
+ if (type == kRequirementAssertCurrentSchemaID) {
+ ICEBERG_ASSIGN_OR_RAISE(auto schema_id,
+ GetJsonValue<int32_t>(json, kCurrentSchemaId));
+ return std::make_unique<table::AssertCurrentSchemaID>(schema_id);
+ }
+ if (type == kRequirementAssertLastAssignedPartitionId) {
+ ICEBERG_ASSIGN_OR_RAISE(auto last_assigned_partition_id,
+ GetJsonValue<int32_t>(json,
kLastAssignedPartitionId));
+ return std::make_unique<table::AssertLastAssignedPartitionId>(
+ last_assigned_partition_id);
+ }
+ if (type == kRequirementAssertDefaultSpecID) {
+ ICEBERG_ASSIGN_OR_RAISE(auto spec_id, GetJsonValue<int32_t>(json,
kDefaultSpecId));
+ return std::make_unique<table::AssertDefaultSpecID>(spec_id);
+ }
+ if (type == kRequirementAssertDefaultSortOrderID) {
+ ICEBERG_ASSIGN_OR_RAISE(auto sort_order_id,
+ GetJsonValue<int32_t>(json, kDefaultSortOrderId));
+ return std::make_unique<table::AssertDefaultSortOrderID>(sort_order_id);
+ }
+
+ return JsonParseError("Unknown table requirement type: {}", type);
+}
+
} // namespace iceberg
diff --git a/src/iceberg/json_internal.h b/src/iceberg/json_internal.h
index 8ca0a676..d55252ca 100644
--- a/src/iceberg/json_internal.h
+++ b/src/iceberg/json_internal.h
@@ -77,43 +77,43 @@ ICEBERG_EXPORT Result<std::unique_ptr<SortOrder>>
SortOrderFromJson(
/// \brief Convert an Iceberg Schema to JSON.
///
-/// \param[in] schema The Iceberg schema to convert.
+/// \param schema The Iceberg schema to convert.
/// \return The JSON representation of the schema.
ICEBERG_EXPORT nlohmann::json ToJson(const Schema& schema);
/// \brief Convert an Iceberg Schema to JSON.
///
-/// \param[in] schema The Iceberg schema to convert.
+/// \param schema The Iceberg schema to convert.
/// \return The JSON string of the schema.
ICEBERG_EXPORT Result<std::string> ToJsonString(const Schema& schema);
/// \brief Convert JSON to an Iceberg Schema.
///
-/// \param[in] json The JSON representation of the schema.
+/// \param json The JSON representation of the schema.
/// \return The Iceberg schema or an error if the conversion fails.
ICEBERG_EXPORT Result<std::unique_ptr<Schema>> SchemaFromJson(const
nlohmann::json& json);
/// \brief Convert an Iceberg Type to JSON.
///
-/// \param[in] type The Iceberg type to convert.
+/// \param type The Iceberg type to convert.
/// \return The JSON representation of the type.
ICEBERG_EXPORT nlohmann::json ToJson(const Type& type);
/// \brief Convert JSON to an Iceberg Type.
///
-/// \param[in] json The JSON representation of the type.
+/// \param json The JSON representation of the type.
/// \return The Iceberg type or an error if the conversion fails.
ICEBERG_EXPORT Result<std::unique_ptr<Type>> TypeFromJson(const
nlohmann::json& json);
/// \brief Convert an Iceberg SchemaField to JSON.
///
-/// \param[in] field The Iceberg field to convert.
+/// \param field The Iceberg field to convert.
/// \return The JSON representation of the field.
ICEBERG_EXPORT nlohmann::json ToJson(const SchemaField& field);
/// \brief Convert JSON to an Iceberg SchemaField.
///
-/// \param[in] json The JSON representation of the field.
+/// \param json The JSON representation of the field.
/// \return The Iceberg field or an error if the conversion fails.
ICEBERG_EXPORT Result<std::unique_ptr<SchemaField>> FieldFromJson(
const nlohmann::json& json);
@@ -185,26 +185,26 @@ ICEBERG_EXPORT Result<std::unique_ptr<PartitionSpec>>
PartitionSpecFromJson(
/// \brief Serializes a `SnapshotRef` object to JSON.
///
-/// \param[in] snapshot_ref The `SnapshotRef` object to be serialized.
+/// \param snapshot_ref The `SnapshotRef` object to be serialized.
/// \return A JSON object representing the `SnapshotRef`.
ICEBERG_EXPORT nlohmann::json ToJson(const SnapshotRef& snapshot_ref);
/// \brief Deserializes a JSON object into a `SnapshotRef` object.
///
-/// \param[in] json The JSON object representing a `SnapshotRef`.
+/// \param json The JSON object representing a `SnapshotRef`.
/// \return A `SnapshotRef` object or an error if the conversion fails.
ICEBERG_EXPORT Result<std::unique_ptr<SnapshotRef>> SnapshotRefFromJson(
const nlohmann::json& json);
/// \brief Serializes a `Snapshot` object to JSON.
///
-/// \param[in] snapshot The `Snapshot` object to be serialized.
+/// \param snapshot The `Snapshot` object to be serialized.
/// \return A JSON object representing the `snapshot`.
ICEBERG_EXPORT nlohmann::json ToJson(const Snapshot& snapshot);
/// \brief Deserializes a JSON object into a `Snapshot` object.
///
-/// \param[in] json The JSON representation of the snapshot.
+/// \param json The JSON representation of the snapshot.
/// \return A `Snapshot` object or an error if the conversion fails.
ICEBERG_EXPORT Result<std::unique_ptr<Snapshot>> SnapshotFromJson(
const nlohmann::json& json);
@@ -295,66 +295,92 @@ ICEBERG_EXPORT Result<std::string> ToJsonString(const
nlohmann::json& json);
/// \brief Serializes a `MappedField` object to JSON.
///
-/// \param[in] field The `MappedField` object to be serialized.
+/// \param field The `MappedField` object to be serialized.
/// \return A JSON object representing the `MappedField`.
ICEBERG_EXPORT nlohmann::json ToJson(const MappedField& field);
/// \brief Deserializes a JSON object into a `MappedField` object.
///
-/// \param[in] json The JSON object representing a `MappedField`.
+/// \param json The JSON object representing a `MappedField`.
/// \return A `MappedField` object or an error if the conversion fails.
ICEBERG_EXPORT Result<MappedField> MappedFieldFromJson(const nlohmann::json&
json);
/// \brief Serializes a `MappedFields` object to JSON.
///
-/// \param[in] mapped_fields The `MappedFields` object to be serialized.
+/// \param mapped_fields The `MappedFields` object to be serialized.
/// \return A JSON object representing the `MappedFields`.
ICEBERG_EXPORT nlohmann::json ToJson(const MappedFields& mapped_fields);
/// \brief Deserializes a JSON object into a `MappedFields` object.
///
-/// \param[in] json The JSON object representing a `MappedFields`.
+/// \param json The JSON object representing a `MappedFields`.
/// \return A `MappedFields` object or an error if the conversion fails.
ICEBERG_EXPORT Result<std::unique_ptr<MappedFields>> MappedFieldsFromJson(
const nlohmann::json& json);
/// \brief Serializes a `NameMapping` object to JSON.
///
-/// \param[in] name_mapping The `NameMapping` object to be serialized.
+/// \param name_mapping The `NameMapping` object to be serialized.
/// \return A JSON object representing the `NameMapping`.
ICEBERG_EXPORT nlohmann::json ToJson(const NameMapping& name_mapping);
/// \brief Deserializes a JSON object into a `NameMapping` object.
///
-/// \param[in] json The JSON object representing a `NameMapping`.
+/// \param json The JSON object representing a `NameMapping`.
/// \return A `NameMapping` object or an error if the conversion fails.
ICEBERG_EXPORT Result<std::unique_ptr<NameMapping>> NameMappingFromJson(
const nlohmann::json& json);
/// \brief Serializes a `TableIdentifier` object to JSON.
///
-/// \param[in] identifier The `TableIdentifier` object to be serialized.
+/// \param identifier The `TableIdentifier` object to be serialized.
/// \return A JSON object representing the `TableIdentifier` in the form of
key-value
/// pairs.
ICEBERG_EXPORT nlohmann::json ToJson(const TableIdentifier& identifier);
/// \brief Deserializes a JSON object into a `TableIdentifier` object.
///
-/// \param[in] json The JSON object representing a `TableIdentifier`.
+/// \param json The JSON object representing a `TableIdentifier`.
/// \return A `TableIdentifier` object or an error if the conversion fails.
ICEBERG_EXPORT Result<TableIdentifier> TableIdentifierFromJson(
const nlohmann::json& json);
/// \brief Serializes a `Namespace` object to JSON.
///
-/// \param[in] ns The `Namespace` object to be serialized.
+/// \param ns The `Namespace` object to be serialized.
/// \return A JSON array representing the namespace levels.
ICEBERG_EXPORT nlohmann::json ToJson(const Namespace& ns);
/// \brief Deserializes a JSON array into a `Namespace` object.
///
-/// \param[in] json The JSON array representing a `Namespace`.
+/// \param json The JSON array representing a `Namespace`.
/// \return A `Namespace` object or an error if the conversion fails.
ICEBERG_EXPORT Result<Namespace> NamespaceFromJson(const nlohmann::json& json);
+/// \brief Serializes a `TableUpdate` object to JSON.
+///
+/// \param update The `TableUpdate` object to be serialized.
+/// \return A JSON object representing the `TableUpdate`.
+ICEBERG_EXPORT nlohmann::json ToJson(const TableUpdate& update);
+
+/// \brief Deserializes a JSON object into a `TableUpdate` object.
+///
+/// \param json The JSON object representing a `TableUpdate`.
+/// \return A `TableUpdate` object or an error if the conversion fails.
+ICEBERG_EXPORT Result<std::unique_ptr<TableUpdate>> TableUpdateFromJson(
+ const nlohmann::json& json);
+
+/// \brief Serializes a `TableRequirement` object to JSON.
+///
+/// \param requirement The `TableRequirement` object to be serialized.
+/// \return A JSON object representing the `TableRequirement`.
+ICEBERG_EXPORT nlohmann::json ToJson(const TableRequirement& requirement);
+
+/// \brief Deserializes a JSON object into a `TableRequirement` object.
+///
+/// \param json The JSON object representing a `TableRequirement`.
+/// \return A `TableRequirement` object or an error if the conversion fails.
+ICEBERG_EXPORT Result<std::unique_ptr<TableRequirement>>
TableRequirementFromJson(
+ const nlohmann::json& json);
+
} // namespace iceberg
diff --git a/src/iceberg/table_requirement.h b/src/iceberg/table_requirement.h
index 82779aec..9b668d4a 100644
--- a/src/iceberg/table_requirement.h
+++ b/src/iceberg/table_requirement.h
@@ -32,6 +32,7 @@
#include "iceberg/iceberg_export.h"
#include "iceberg/result.h"
#include "iceberg/type_fwd.h"
+#include "iceberg/util/checked_cast.h"
namespace iceberg {
@@ -63,6 +64,22 @@ class ICEBERG_EXPORT TableRequirement {
/// \param base The base table metadata to validate against (may be nullptr)
/// \return Status indicating success or failure with error details
virtual Status Validate(const TableMetadata* base) const = 0;
+
+ /// \brief Check equality with another TableRequirement
+ ///
+ /// \param other The requirement to compare with
+ /// \return true if the requirements are equal, false otherwise
+ virtual bool Equals(const TableRequirement& other) const = 0;
+
+ /// \brief Create a deep copy of this requirement
+ ///
+ /// \return A unique_ptr to a new TableRequirement that is a copy of this one
+ virtual std::unique_ptr<TableRequirement> Clone() const = 0;
+
+ /// \brief Compare two TableRequirement instances for equality
+ friend bool operator==(const TableRequirement& lhs, const TableRequirement&
rhs) {
+ return lhs.Equals(rhs);
+ }
};
namespace table {
@@ -78,6 +95,14 @@ class ICEBERG_EXPORT AssertDoesNotExist : public
TableRequirement {
Kind kind() const override { return Kind::kAssertDoesNotExist; }
Status Validate(const TableMetadata* base) const override;
+
+ bool Equals(const TableRequirement& other) const override {
+ return other.kind() == Kind::kAssertDoesNotExist;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return std::make_unique<AssertDoesNotExist>();
+ }
};
/// \brief Requirement that the table UUID matches the expected value
@@ -94,6 +119,18 @@ class ICEBERG_EXPORT AssertUUID : public TableRequirement {
Status Validate(const TableMetadata* base) const override;
+ bool Equals(const TableRequirement& other) const override {
+ if (other.kind() != Kind::kAssertUUID) {
+ return false;
+ }
+ const auto& other_uuid = internal::checked_cast<const AssertUUID&>(other);
+ return uuid_ == other_uuid.uuid_;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return std::make_unique<AssertUUID>(uuid_);
+ }
+
private:
std::string uuid_;
};
@@ -116,6 +153,18 @@ class ICEBERG_EXPORT AssertRefSnapshotID : public
TableRequirement {
Status Validate(const TableMetadata* base) const override;
+ bool Equals(const TableRequirement& other) const override {
+ if (other.kind() != Kind::kAssertRefSnapshotID) {
+ return false;
+ }
+ const auto& other_ref = internal::checked_cast<const
AssertRefSnapshotID&>(other);
+ return ref_name_ == other_ref.ref_name_ && snapshot_id_ ==
other_ref.snapshot_id_;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return std::make_unique<AssertRefSnapshotID>(ref_name_, snapshot_id_);
+ }
+
private:
std::string ref_name_;
std::optional<int64_t> snapshot_id_;
@@ -136,6 +185,19 @@ class ICEBERG_EXPORT AssertLastAssignedFieldId : public
TableRequirement {
Status Validate(const TableMetadata* base) const override;
+ bool Equals(const TableRequirement& other) const override {
+ if (other.kind() != Kind::kAssertLastAssignedFieldId) {
+ return false;
+ }
+ const auto& other_field =
+ internal::checked_cast<const AssertLastAssignedFieldId&>(other);
+ return last_assigned_field_id_ == other_field.last_assigned_field_id_;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return
std::make_unique<AssertLastAssignedFieldId>(last_assigned_field_id_);
+ }
+
private:
int32_t last_assigned_field_id_;
};
@@ -154,6 +216,19 @@ class ICEBERG_EXPORT AssertCurrentSchemaID : public
TableRequirement {
Status Validate(const TableMetadata* base) const override;
+ bool Equals(const TableRequirement& other) const override {
+ if (other.kind() != Kind::kAssertCurrentSchemaID) {
+ return false;
+ }
+ const auto& other_schema =
+ internal::checked_cast<const AssertCurrentSchemaID&>(other);
+ return schema_id_ == other_schema.schema_id_;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return std::make_unique<AssertCurrentSchemaID>(schema_id_);
+ }
+
private:
int32_t schema_id_;
};
@@ -173,6 +248,19 @@ class ICEBERG_EXPORT AssertLastAssignedPartitionId :
public TableRequirement {
Status Validate(const TableMetadata* base) const override;
+ bool Equals(const TableRequirement& other) const override {
+ if (other.kind() != Kind::kAssertLastAssignedPartitionId) {
+ return false;
+ }
+ const auto& other_partition =
+ internal::checked_cast<const AssertLastAssignedPartitionId&>(other);
+ return last_assigned_partition_id_ ==
other_partition.last_assigned_partition_id_;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return
std::make_unique<AssertLastAssignedPartitionId>(last_assigned_partition_id_);
+ }
+
private:
int32_t last_assigned_partition_id_;
};
@@ -191,6 +279,18 @@ class ICEBERG_EXPORT AssertDefaultSpecID : public
TableRequirement {
Status Validate(const TableMetadata* base) const override;
+ bool Equals(const TableRequirement& other) const override {
+ if (other.kind() != Kind::kAssertDefaultSpecID) {
+ return false;
+ }
+ const auto& other_spec = internal::checked_cast<const
AssertDefaultSpecID&>(other);
+ return spec_id_ == other_spec.spec_id_;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return std::make_unique<AssertDefaultSpecID>(spec_id_);
+ }
+
private:
int32_t spec_id_;
};
@@ -210,6 +310,19 @@ class ICEBERG_EXPORT AssertDefaultSortOrderID : public
TableRequirement {
Status Validate(const TableMetadata* base) const override;
+ bool Equals(const TableRequirement& other) const override {
+ if (other.kind() != Kind::kAssertDefaultSortOrderID) {
+ return false;
+ }
+ const auto& other_sort =
+ internal::checked_cast<const AssertDefaultSortOrderID&>(other);
+ return sort_order_id_ == other_sort.sort_order_id_;
+ }
+
+ std::unique_ptr<TableRequirement> Clone() const override {
+ return std::make_unique<AssertDefaultSortOrderID>(sort_order_id_);
+ }
+
private:
int32_t sort_order_id_;
};
diff --git a/src/iceberg/table_update.cc b/src/iceberg/table_update.cc
index 91578977..38ce0fbc 100644
--- a/src/iceberg/table_update.cc
+++ b/src/iceberg/table_update.cc
@@ -20,6 +20,8 @@
#include "iceberg/table_update.h"
#include "iceberg/exception.h"
+#include "iceberg/schema.h"
+#include "iceberg/sort_order.h"
#include "iceberg/table_metadata.h"
#include "iceberg/table_requirements.h"
@@ -39,6 +41,18 @@ void AssignUUID::GenerateRequirements(TableUpdateContext&
context) const {
// AssignUUID does not generate additional requirements.
}
+bool AssignUUID::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kAssignUUID) {
+ return false;
+ }
+ const auto& other_assign = static_cast<const AssignUUID&>(other);
+ return uuid_ == other_assign.uuid_;
+}
+
+std::unique_ptr<TableUpdate> AssignUUID::Clone() const {
+ return std::make_unique<AssignUUID>(uuid_);
+}
+
// UpgradeFormatVersion
void UpgradeFormatVersion::ApplyTo(TableMetadataBuilder& builder) const {
@@ -49,6 +63,18 @@ void
UpgradeFormatVersion::GenerateRequirements(TableUpdateContext& context) con
// UpgradeFormatVersion doesn't generate any requirements
}
+bool UpgradeFormatVersion::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kUpgradeFormatVersion) {
+ return false;
+ }
+ const auto& other_upgrade = static_cast<const UpgradeFormatVersion&>(other);
+ return format_version_ == other_upgrade.format_version_;
+}
+
+std::unique_ptr<TableUpdate> UpgradeFormatVersion::Clone() const {
+ return std::make_unique<UpgradeFormatVersion>(format_version_);
+}
+
// AddSchema
void AddSchema::ApplyTo(TableMetadataBuilder& builder) const {
@@ -59,6 +85,24 @@ void AddSchema::GenerateRequirements(TableUpdateContext&
context) const {
context.RequireLastAssignedFieldIdUnchanged();
}
+bool AddSchema::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kAddSchema) {
+ return false;
+ }
+ const auto& other_add = internal::checked_cast<const AddSchema&>(other);
+ if (!schema_ != !other_add.schema_) {
+ return false;
+ }
+ if (schema_ && !(*schema_ == *other_add.schema_)) {
+ return false;
+ }
+ return last_column_id_ == other_add.last_column_id_;
+}
+
+std::unique_ptr<TableUpdate> AddSchema::Clone() const {
+ return std::make_unique<AddSchema>(schema_, last_column_id_);
+}
+
// SetCurrentSchema
void SetCurrentSchema::ApplyTo(TableMetadataBuilder& builder) const {
@@ -69,6 +113,18 @@ void
SetCurrentSchema::GenerateRequirements(TableUpdateContext& context) const {
context.RequireCurrentSchemaIdUnchanged();
}
+bool SetCurrentSchema::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kSetCurrentSchema) {
+ return false;
+ }
+ const auto& other_set = static_cast<const SetCurrentSchema&>(other);
+ return schema_id_ == other_set.schema_id_;
+}
+
+std::unique_ptr<TableUpdate> SetCurrentSchema::Clone() const {
+ return std::make_unique<SetCurrentSchema>(schema_id_);
+}
+
// AddPartitionSpec
void AddPartitionSpec::ApplyTo(TableMetadataBuilder& builder) const {
@@ -79,6 +135,24 @@ void
AddPartitionSpec::GenerateRequirements(TableUpdateContext& context) const {
context.RequireLastAssignedPartitionIdUnchanged();
}
+bool AddPartitionSpec::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kAddPartitionSpec) {
+ return false;
+ }
+ const auto& other_add = internal::checked_cast<const
AddPartitionSpec&>(other);
+ if (!spec_ != !other_add.spec_) {
+ return false;
+ }
+ if (spec_ && *spec_ != *other_add.spec_) {
+ return false;
+ }
+ return true;
+}
+
+std::unique_ptr<TableUpdate> AddPartitionSpec::Clone() const {
+ return std::make_unique<AddPartitionSpec>(spec_);
+}
+
// SetDefaultPartitionSpec
void SetDefaultPartitionSpec::ApplyTo(TableMetadataBuilder& builder) const {
@@ -89,6 +163,18 @@ void
SetDefaultPartitionSpec::GenerateRequirements(TableUpdateContext& context)
context.RequireDefaultSpecIdUnchanged();
}
+bool SetDefaultPartitionSpec::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kSetDefaultPartitionSpec) {
+ return false;
+ }
+ const auto& other_set = static_cast<const SetDefaultPartitionSpec&>(other);
+ return spec_id_ == other_set.spec_id_;
+}
+
+std::unique_ptr<TableUpdate> SetDefaultPartitionSpec::Clone() const {
+ return std::make_unique<SetDefaultPartitionSpec>(spec_id_);
+}
+
// RemovePartitionSpecs
void RemovePartitionSpecs::ApplyTo(TableMetadataBuilder& builder) const {
@@ -100,6 +186,18 @@ void
RemovePartitionSpecs::GenerateRequirements(TableUpdateContext& context) con
context.RequireNoBranchesChanged();
}
+bool RemovePartitionSpecs::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kRemovePartitionSpecs) {
+ return false;
+ }
+ const auto& other_remove = static_cast<const RemovePartitionSpecs&>(other);
+ return spec_ids_ == other_remove.spec_ids_;
+}
+
+std::unique_ptr<TableUpdate> RemovePartitionSpecs::Clone() const {
+ return std::make_unique<RemovePartitionSpecs>(spec_ids_);
+}
+
// RemoveSchemas
void RemoveSchemas::ApplyTo(TableMetadataBuilder& builder) const {
@@ -111,6 +209,18 @@ void
RemoveSchemas::GenerateRequirements(TableUpdateContext& context) const {
context.RequireNoBranchesChanged();
}
+bool RemoveSchemas::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kRemoveSchemas) {
+ return false;
+ }
+ const auto& other_remove = static_cast<const RemoveSchemas&>(other);
+ return schema_ids_ == other_remove.schema_ids_;
+}
+
+std::unique_ptr<TableUpdate> RemoveSchemas::Clone() const {
+ return std::make_unique<RemoveSchemas>(schema_ids_);
+}
+
// AddSortOrder
void AddSortOrder::ApplyTo(TableMetadataBuilder& builder) const {
@@ -121,6 +231,24 @@ void
AddSortOrder::GenerateRequirements(TableUpdateContext& context) const {
// AddSortOrder doesn't generate any requirements
}
+bool AddSortOrder::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kAddSortOrder) {
+ return false;
+ }
+ const auto& other_add = internal::checked_cast<const AddSortOrder&>(other);
+ if (!sort_order_ != !other_add.sort_order_) {
+ return false;
+ }
+ if (sort_order_ && !(*sort_order_ == *other_add.sort_order_)) {
+ return false;
+ }
+ return true;
+}
+
+std::unique_ptr<TableUpdate> AddSortOrder::Clone() const {
+ return std::make_unique<AddSortOrder>(sort_order_);
+}
+
// SetDefaultSortOrder
void SetDefaultSortOrder::ApplyTo(TableMetadataBuilder& builder) const {
@@ -131,6 +259,18 @@ void
SetDefaultSortOrder::GenerateRequirements(TableUpdateContext& context) cons
context.RequireDefaultSortOrderIdUnchanged();
}
+bool SetDefaultSortOrder::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kSetDefaultSortOrder) {
+ return false;
+ }
+ const auto& other_set = static_cast<const SetDefaultSortOrder&>(other);
+ return sort_order_id_ == other_set.sort_order_id_;
+}
+
+std::unique_ptr<TableUpdate> SetDefaultSortOrder::Clone() const {
+ return std::make_unique<SetDefaultSortOrder>(sort_order_id_);
+}
+
// AddSnapshot
void AddSnapshot::ApplyTo(TableMetadataBuilder& builder) const {
@@ -141,6 +281,24 @@ void AddSnapshot::GenerateRequirements(TableUpdateContext&
context) const {
// AddSnapshot doesn't generate any requirements
}
+bool AddSnapshot::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kAddSnapshot) {
+ return false;
+ }
+ const auto& other_add = internal::checked_cast<const AddSnapshot&>(other);
+ if (!snapshot_ != !other_add.snapshot_) {
+ return false;
+ }
+ if (snapshot_ && *snapshot_ != *other_add.snapshot_) {
+ return false;
+ }
+ return true;
+}
+
+std::unique_ptr<TableUpdate> AddSnapshot::Clone() const {
+ return std::make_unique<AddSnapshot>(snapshot_);
+}
+
// RemoveSnapshots
void RemoveSnapshots::ApplyTo(TableMetadataBuilder& builder) const {}
@@ -149,6 +307,18 @@ void
RemoveSnapshots::GenerateRequirements(TableUpdateContext& context) const {
// RemoveSnapshots doesn't generate any requirements
}
+bool RemoveSnapshots::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kRemoveSnapshots) {
+ return false;
+ }
+ const auto& other_remove = static_cast<const RemoveSnapshots&>(other);
+ return snapshot_ids_ == other_remove.snapshot_ids_;
+}
+
+std::unique_ptr<TableUpdate> RemoveSnapshots::Clone() const {
+ return std::make_unique<RemoveSnapshots>(snapshot_ids_);
+}
+
// RemoveSnapshotRef
void RemoveSnapshotRef::ApplyTo(TableMetadataBuilder& builder) const {
@@ -159,6 +329,18 @@ void
RemoveSnapshotRef::GenerateRequirements(TableUpdateContext& context) const
// RemoveSnapshotRef doesn't generate any requirements
}
+bool RemoveSnapshotRef::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kRemoveSnapshotRef) {
+ return false;
+ }
+ const auto& other_remove = static_cast<const RemoveSnapshotRef&>(other);
+ return ref_name_ == other_remove.ref_name_;
+}
+
+std::unique_ptr<TableUpdate> RemoveSnapshotRef::Clone() const {
+ return std::make_unique<RemoveSnapshotRef>(ref_name_);
+}
+
// SetSnapshotRef
void SetSnapshotRef::ApplyTo(TableMetadataBuilder& builder) const {
@@ -178,6 +360,24 @@ void
SetSnapshotRef::GenerateRequirements(TableUpdateContext& context) const {
}
}
+bool SetSnapshotRef::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kSetSnapshotRef) {
+ return false;
+ }
+ const auto& other_set = static_cast<const SetSnapshotRef&>(other);
+ return ref_name_ == other_set.ref_name_ && snapshot_id_ ==
other_set.snapshot_id_ &&
+ type_ == other_set.type_ &&
+ min_snapshots_to_keep_ == other_set.min_snapshots_to_keep_ &&
+ max_snapshot_age_ms_ == other_set.max_snapshot_age_ms_ &&
+ max_ref_age_ms_ == other_set.max_ref_age_ms_;
+}
+
+std::unique_ptr<TableUpdate> SetSnapshotRef::Clone() const {
+ return std::make_unique<SetSnapshotRef>(ref_name_, snapshot_id_, type_,
+ min_snapshots_to_keep_,
max_snapshot_age_ms_,
+ max_ref_age_ms_);
+}
+
// SetProperties
void SetProperties::ApplyTo(TableMetadataBuilder& builder) const {
@@ -188,6 +388,18 @@ void
SetProperties::GenerateRequirements(TableUpdateContext& context) const {
// SetProperties doesn't generate any requirements
}
+bool SetProperties::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kSetProperties) {
+ return false;
+ }
+ const auto& other_set = static_cast<const SetProperties&>(other);
+ return updated_ == other_set.updated_;
+}
+
+std::unique_ptr<TableUpdate> SetProperties::Clone() const {
+ return std::make_unique<SetProperties>(updated_);
+}
+
// RemoveProperties
void RemoveProperties::ApplyTo(TableMetadataBuilder& builder) const {
@@ -198,6 +410,18 @@ void
RemoveProperties::GenerateRequirements(TableUpdateContext& context) const {
// RemoveProperties doesn't generate any requirements
}
+bool RemoveProperties::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kRemoveProperties) {
+ return false;
+ }
+ const auto& other_remove = static_cast<const RemoveProperties&>(other);
+ return removed_ == other_remove.removed_;
+}
+
+std::unique_ptr<TableUpdate> RemoveProperties::Clone() const {
+ return std::make_unique<RemoveProperties>(removed_);
+}
+
// SetLocation
void SetLocation::ApplyTo(TableMetadataBuilder& builder) const {
@@ -208,4 +432,16 @@ void SetLocation::GenerateRequirements(TableUpdateContext&
context) const {
// SetLocation doesn't generate any requirements
}
+bool SetLocation::Equals(const TableUpdate& other) const {
+ if (other.kind() != Kind::kSetLocation) {
+ return false;
+ }
+ const auto& other_set = static_cast<const SetLocation&>(other);
+ return location_ == other_set.location_;
+}
+
+std::unique_ptr<TableUpdate> SetLocation::Clone() const {
+ return std::make_unique<SetLocation>(location_);
+}
+
} // namespace iceberg::table
diff --git a/src/iceberg/table_update.h b/src/iceberg/table_update.h
index a6bdb9e5..87524319 100644
--- a/src/iceberg/table_update.h
+++ b/src/iceberg/table_update.h
@@ -83,6 +83,22 @@ class ICEBERG_EXPORT TableUpdate {
///
/// \param context The context containing base metadata and operation state
virtual void GenerateRequirements(TableUpdateContext& context) const = 0;
+
+ /// \brief Check equality with another TableUpdate
+ ///
+ /// \param other The update to compare with
+ /// \return true if the updates are equal, false otherwise
+ virtual bool Equals(const TableUpdate& other) const = 0;
+
+ /// \brief Create a deep copy of this update
+ ///
+ /// \return A unique_ptr to a new TableUpdate that is a copy of this one
+ virtual std::unique_ptr<TableUpdate> Clone() const = 0;
+
+ /// \brief Compare two TableUpdate instances for equality
+ friend bool operator==(const TableUpdate& lhs, const TableUpdate& rhs) {
+ return lhs.Equals(rhs);
+ }
};
namespace table {
@@ -100,6 +116,10 @@ class ICEBERG_EXPORT AssignUUID : public TableUpdate {
Kind kind() const override { return Kind::kAssignUUID; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::string uuid_;
};
@@ -118,6 +138,10 @@ class ICEBERG_EXPORT UpgradeFormatVersion : public
TableUpdate {
Kind kind() const override { return Kind::kUpgradeFormatVersion; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
int8_t format_version_;
};
@@ -138,6 +162,10 @@ class ICEBERG_EXPORT AddSchema : public TableUpdate {
Kind kind() const override { return Kind::kAddSchema; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::shared_ptr<Schema> schema_;
int32_t last_column_id_;
@@ -156,6 +184,10 @@ class ICEBERG_EXPORT SetCurrentSchema : public TableUpdate
{
Kind kind() const override { return Kind::kSetCurrentSchema; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
int32_t schema_id_;
};
@@ -174,6 +206,10 @@ class ICEBERG_EXPORT AddPartitionSpec : public TableUpdate
{
Kind kind() const override { return Kind::kAddPartitionSpec; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::shared_ptr<PartitionSpec> spec_;
};
@@ -191,6 +227,10 @@ class ICEBERG_EXPORT SetDefaultPartitionSpec : public
TableUpdate {
Kind kind() const override { return Kind::kSetDefaultPartitionSpec; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
int32_t spec_id_;
};
@@ -209,6 +249,10 @@ class ICEBERG_EXPORT RemovePartitionSpecs : public
TableUpdate {
Kind kind() const override { return Kind::kRemovePartitionSpecs; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::vector<int32_t> spec_ids_;
};
@@ -227,6 +271,10 @@ class ICEBERG_EXPORT RemoveSchemas : public TableUpdate {
Kind kind() const override { return Kind::kRemoveSchemas; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::unordered_set<int32_t> schema_ids_;
};
@@ -245,6 +293,10 @@ class ICEBERG_EXPORT AddSortOrder : public TableUpdate {
Kind kind() const override { return Kind::kAddSortOrder; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::shared_ptr<SortOrder> sort_order_;
};
@@ -262,6 +314,10 @@ class ICEBERG_EXPORT SetDefaultSortOrder : public
TableUpdate {
Kind kind() const override { return Kind::kSetDefaultSortOrder; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
int32_t sort_order_id_;
};
@@ -280,6 +336,10 @@ class ICEBERG_EXPORT AddSnapshot : public TableUpdate {
Kind kind() const override { return Kind::kAddSnapshot; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::shared_ptr<Snapshot> snapshot_;
};
@@ -298,6 +358,10 @@ class ICEBERG_EXPORT RemoveSnapshots : public TableUpdate {
Kind kind() const override { return Kind::kRemoveSnapshots; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::vector<int64_t> snapshot_ids_;
};
@@ -315,6 +379,10 @@ class ICEBERG_EXPORT RemoveSnapshotRef : public
TableUpdate {
Kind kind() const override { return Kind::kRemoveSnapshotRef; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::string ref_name_;
};
@@ -350,6 +418,10 @@ class ICEBERG_EXPORT SetSnapshotRef : public TableUpdate {
Kind kind() const override { return Kind::kSetSnapshotRef; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::string ref_name_;
int64_t snapshot_id_;
@@ -373,6 +445,10 @@ class ICEBERG_EXPORT SetProperties : public TableUpdate {
Kind kind() const override { return Kind::kSetProperties; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::unordered_map<std::string, std::string> updated_;
};
@@ -391,6 +467,10 @@ class ICEBERG_EXPORT RemoveProperties : public TableUpdate
{
Kind kind() const override { return Kind::kRemoveProperties; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::unordered_set<std::string> removed_;
};
@@ -408,6 +488,10 @@ class ICEBERG_EXPORT SetLocation : public TableUpdate {
Kind kind() const override { return Kind::kSetLocation; }
+ bool Equals(const TableUpdate& other) const override;
+
+ std::unique_ptr<TableUpdate> Clone() const override;
+
private:
std::string location_;
};
diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt
index e93852aa..bb3d97b4 100644
--- a/src/iceberg/test/CMakeLists.txt
+++ b/src/iceberg/test/CMakeLists.txt
@@ -49,6 +49,10 @@ function(add_iceberg_test test_name)
target_link_libraries(${test_name} PRIVATE iceberg_static
GTest::gmock_main)
endif()
+ if(MSVC_TOOLCHAIN)
+ target_compile_options(${test_name} PRIVATE /bigobj)
+ endif()
+
add_test(NAME ${test_name} COMMAND ${test_name})
endfunction()
@@ -187,6 +191,9 @@ if(ICEBERG_BUILD_REST)
target_include_directories(${test_name} PRIVATE
"${CMAKE_BINARY_DIR}/iceberg/test/")
target_sources(${test_name} PRIVATE ${ARG_SOURCES})
target_link_libraries(${test_name} PRIVATE GTest::gmock_main
iceberg_rest_static)
+ if(MSVC_TOOLCHAIN)
+ target_compile_options(${test_name} PRIVATE /bigobj)
+ endif()
add_test(NAME ${test_name} COMMAND ${test_name})
endfunction()
diff --git a/src/iceberg/test/json_internal_test.cc
b/src/iceberg/test/json_internal_test.cc
index a23cc680..f88527ff 100644
--- a/src/iceberg/test/json_internal_test.cc
+++ b/src/iceberg/test/json_internal_test.cc
@@ -31,6 +31,8 @@
#include "iceberg/snapshot.h"
#include "iceberg/sort_field.h"
#include "iceberg/sort_order.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_update.h"
#include "iceberg/test/matchers.h"
#include "iceberg/transform.h"
#include "iceberg/util/formatter.h" // IWYU pragma: keep
@@ -293,4 +295,340 @@ TEST(JsonInternalTest, NameMapping) {
TestJsonConversion(*mapping, expected_json);
}
+// TableUpdate JSON Serialization/Deserialization Tests
+TEST(JsonInternalTest, TableUpdateAssignUUID) {
+ table::AssignUUID update("550e8400-e29b-41d4-a716-446655440000");
+ nlohmann::json expected =
+
R"({"action":"assign-uuid","uuid":"550e8400-e29b-41d4-a716-446655440000"})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+ EXPECT_EQ(*internal::checked_cast<table::AssignUUID*>(parsed.value().get()),
update);
+}
+
+TEST(JsonInternalTest, TableUpdateUpgradeFormatVersion) {
+ table::UpgradeFormatVersion update(2);
+ nlohmann::json expected =
+ R"({"action":"upgrade-format-version","format-version":2})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::UpgradeFormatVersion*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateAddSchema) {
+ auto schema = std::make_shared<Schema>(
+ std::vector<SchemaField>{SchemaField(1, "id", int64(), false),
+ SchemaField(2, "name", string(), true)},
+ /*schema_id=*/1);
+ table::AddSchema update(schema, 2);
+
+ auto json = ToJson(update);
+ EXPECT_EQ(json["action"], "add-schema");
+ EXPECT_EQ(json["last-column-id"], 2);
+ EXPECT_TRUE(json.contains("schema"));
+
+ auto parsed = TableUpdateFromJson(json);
+ ASSERT_THAT(parsed, IsOk());
+ auto* actual =
internal::checked_cast<table::AddSchema*>(parsed.value().get());
+ EXPECT_EQ(actual->last_column_id(), update.last_column_id());
+ EXPECT_EQ(*actual->schema(), *update.schema());
+}
+
+TEST(JsonInternalTest, TableUpdateSetCurrentSchema) {
+ table::SetCurrentSchema update(1);
+ nlohmann::json expected =
R"({"action":"set-current-schema","schema-id":1})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::SetCurrentSchema*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateSetDefaultPartitionSpec) {
+ table::SetDefaultPartitionSpec update(2);
+ nlohmann::json expected =
R"({"action":"set-default-spec","spec-id":2})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+ EXPECT_EQ(
+
*internal::checked_cast<table::SetDefaultPartitionSpec*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateRemovePartitionSpecs) {
+ table::RemovePartitionSpecs update({1, 2, 3});
+ nlohmann::json expected =
+ R"({"action":"remove-partition-specs","spec-ids":[1,2,3]})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::RemovePartitionSpecs*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateRemoveSchemas) {
+ table::RemoveSchemas update({1, 2});
+
+ auto json = ToJson(update);
+ EXPECT_EQ(json["action"], "remove-schemas");
+ EXPECT_THAT(json["schema-ids"].get<std::vector<int32_t>>(),
+ testing::UnorderedElementsAre(1, 2));
+
+ auto parsed = TableUpdateFromJson(json);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::RemoveSchemas*>(parsed.value().get()),
update);
+}
+
+TEST(JsonInternalTest, TableUpdateSetDefaultSortOrder) {
+ table::SetDefaultSortOrder update(1);
+ nlohmann::json expected =
+ R"({"action":"set-default-sort-order","sort-order-id":1})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::SetDefaultSortOrder*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateAddSnapshot) {
+ auto snapshot = std::make_shared<Snapshot>(
+ Snapshot{.snapshot_id = 123456789,
+ .parent_snapshot_id = 987654321,
+ .sequence_number = 5,
+ .timestamp_ms = TimePointMsFromUnixMs(1234567890000).value(),
+ .manifest_list = "/path/to/manifest-list.avro",
+ .summary = {{SnapshotSummaryFields::kOperation,
DataOperation::kAppend}},
+ .schema_id = 1});
+ table::AddSnapshot update(snapshot);
+
+ auto json = ToJson(update);
+ EXPECT_EQ(json["action"], "add-snapshot");
+ EXPECT_TRUE(json.contains("snapshot"));
+
+ auto parsed = TableUpdateFromJson(json);
+ ASSERT_THAT(parsed, IsOk());
+ auto* actual =
internal::checked_cast<table::AddSnapshot*>(parsed.value().get());
+ EXPECT_EQ(*actual->snapshot(), *update.snapshot());
+}
+
+TEST(JsonInternalTest, TableUpdateRemoveSnapshots) {
+ table::RemoveSnapshots update({111, 222, 333});
+ nlohmann::json expected =
+ R"({"action":"remove-snapshots","snapshot-ids":[111,222,333]})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::RemoveSnapshots*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateRemoveSnapshotRef) {
+ table::RemoveSnapshotRef update("my-branch");
+ nlohmann::json expected =
+ R"({"action":"remove-snapshot-ref","ref-name":"my-branch"})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::RemoveSnapshotRef*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateSetSnapshotRefBranch) {
+ table::SetSnapshotRef update("main", 123456789, SnapshotRefType::kBranch, 5,
86400000,
+ 604800000);
+
+ auto json = ToJson(update);
+ EXPECT_EQ(json["action"], "set-snapshot-ref");
+ EXPECT_EQ(json["ref-name"], "main");
+ EXPECT_EQ(json["snapshot-id"], 123456789);
+ EXPECT_EQ(json["type"], "branch");
+
+ auto parsed = TableUpdateFromJson(json);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::SetSnapshotRef*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateSetSnapshotRefTag) {
+ table::SetSnapshotRef update("release-1.0", 987654321,
SnapshotRefType::kTag);
+
+ auto json = ToJson(update);
+ EXPECT_EQ(json["action"], "set-snapshot-ref");
+ EXPECT_EQ(json["type"], "tag");
+
+ auto parsed = TableUpdateFromJson(json);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::SetSnapshotRef*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateSetProperties) {
+ table::SetProperties update({{"key1", "value1"}, {"key2", "value2"}});
+
+ auto json = ToJson(update);
+ EXPECT_EQ(json["action"], "set-properties");
+ EXPECT_TRUE(json.contains("updates"));
+
+ auto parsed = TableUpdateFromJson(json);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::SetProperties*>(parsed.value().get()),
update);
+}
+
+TEST(JsonInternalTest, TableUpdateRemoveProperties) {
+ table::RemoveProperties update({"key1", "key2"});
+
+ auto json = ToJson(update);
+ EXPECT_EQ(json["action"], "remove-properties");
+ EXPECT_TRUE(json.contains("removals"));
+
+ auto parsed = TableUpdateFromJson(json);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::RemoveProperties*>(parsed.value().get()),
+ update);
+}
+
+TEST(JsonInternalTest, TableUpdateSetLocation) {
+ table::SetLocation update("s3://bucket/warehouse/table");
+ nlohmann::json expected =
+
R"({"action":"set-location","location":"s3://bucket/warehouse/table"})"_json;
+
+ EXPECT_EQ(ToJson(update), expected);
+ auto parsed = TableUpdateFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::SetLocation*>(parsed.value().get()),
update);
+}
+
+TEST(JsonInternalTest, TableUpdateUnknownAction) {
+ nlohmann::json json = R"({"action":"unknown-action"})"_json;
+ auto result = TableUpdateFromJson(json);
+ EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+ EXPECT_THAT(result, HasErrorMessage("Unknown table update action"));
+}
+
+// TableRequirement JSON Serialization/Deserialization Tests
+TEST(TableRequirementJsonTest, TableRequirementAssertDoesNotExist) {
+ table::AssertDoesNotExist req;
+ nlohmann::json expected = R"({"type":"assert-create"})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+ EXPECT_EQ(parsed.value()->kind(),
TableRequirement::Kind::kAssertDoesNotExist);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertUUID) {
+ table::AssertUUID req("550e8400-e29b-41d4-a716-446655440000");
+ nlohmann::json expected =
+
R"({"type":"assert-table-uuid","uuid":"550e8400-e29b-41d4-a716-446655440000"})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+ EXPECT_EQ(*internal::checked_cast<table::AssertUUID*>(parsed.value().get()),
req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertRefSnapshotID) {
+ table::AssertRefSnapshotID req("main", 123456789);
+ nlohmann::json expected =
+
R"({"type":"assert-ref-snapshot-id","ref-name":"main","snapshot-id":123456789})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::AssertRefSnapshotID*>(parsed.value().get()),
+ req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertRefSnapshotIDWithNull) {
+ table::AssertRefSnapshotID req("main", std::nullopt);
+ nlohmann::json expected =
+
R"({"type":"assert-ref-snapshot-id","ref-name":"main","snapshot-id":null})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::AssertRefSnapshotID*>(parsed.value().get()),
+ req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertLastAssignedFieldId) {
+ table::AssertLastAssignedFieldId req(100);
+ nlohmann::json expected =
+
R"({"type":"assert-last-assigned-field-id","last-assigned-field-id":100})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+ EXPECT_EQ(
+
*internal::checked_cast<table::AssertLastAssignedFieldId*>(parsed.value().get()),
+ req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertCurrentSchemaID) {
+ table::AssertCurrentSchemaID req(1);
+ nlohmann::json expected =
+ R"({"type":"assert-current-schema-id","current-schema-id":1})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::AssertCurrentSchemaID*>(parsed.value().get()),
+ req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertLastAssignedPartitionId) {
+ table::AssertLastAssignedPartitionId req(1000);
+ nlohmann::json expected =
+
R"({"type":"assert-last-assigned-partition-id","last-assigned-partition-id":1000})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+ EXPECT_EQ(*internal::checked_cast<table::AssertLastAssignedPartitionId*>(
+ parsed.value().get()),
+ req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertDefaultSpecID) {
+ table::AssertDefaultSpecID req(0);
+ nlohmann::json expected =
+ R"({"type":"assert-default-spec-id","default-spec-id":0})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+
EXPECT_EQ(*internal::checked_cast<table::AssertDefaultSpecID*>(parsed.value().get()),
+ req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementAssertDefaultSortOrderID) {
+ table::AssertDefaultSortOrderID req(0);
+ nlohmann::json expected =
+
R"({"type":"assert-default-sort-order-id","default-sort-order-id":0})"_json;
+
+ EXPECT_EQ(ToJson(req), expected);
+ auto parsed = TableRequirementFromJson(expected);
+ ASSERT_THAT(parsed, IsOk());
+ EXPECT_EQ(
+
*internal::checked_cast<table::AssertDefaultSortOrderID*>(parsed.value().get()),
+ req);
+}
+
+TEST(TableRequirementJsonTest, TableRequirementUnknownType) {
+ nlohmann::json json = R"({"type":"unknown-type"})"_json;
+ auto result = TableRequirementFromJson(json);
+ EXPECT_THAT(result, IsError(ErrorKind::kJsonParseError));
+ EXPECT_THAT(result, HasErrorMessage("Unknown table requirement type"));
+}
+
} // namespace iceberg
diff --git a/src/iceberg/test/rest_catalog_test.cc
b/src/iceberg/test/rest_catalog_test.cc
index 52969040..7f04de0a 100644
--- a/src/iceberg/test/rest_catalog_test.cc
+++ b/src/iceberg/test/rest_catalog_test.cc
@@ -21,6 +21,7 @@
#include <unistd.h>
+#include <algorithm>
#include <chrono>
#include <memory>
#include <print>
@@ -45,6 +46,8 @@
#include "iceberg/sort_order.h"
#include "iceberg/table.h"
#include "iceberg/table_identifier.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_update.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/std_io.h"
#include "iceberg/test/test_resource.h"
@@ -407,6 +410,45 @@ TEST_F(RestCatalogIntegrationTest, CreateTable) {
HasErrorMessage("Table already exists:
test_create_table.apple.ios.t1"));
}
+TEST_F(RestCatalogIntegrationTest, ListTables) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace
+ Namespace ns{.levels = {"test_list_tables"}};
+ auto status = catalog->CreateNamespace(ns, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Initially no tables
+ auto list_result = catalog->ListTables(ns);
+ ASSERT_THAT(list_result, IsOk());
+ EXPECT_TRUE(list_result.value().empty());
+
+ // Create tables
+ auto schema = CreateDefaultSchema();
+ auto partition_spec = PartitionSpec::Unpartitioned();
+ auto sort_order = SortOrder::Unsorted();
+ std::unordered_map<std::string, std::string> table_properties;
+
+ TableIdentifier table1{.ns = ns, .name = "table1"};
+ auto create_result = catalog->CreateTable(table1, schema, partition_spec,
sort_order,
+ "", table_properties);
+ ASSERT_THAT(create_result, IsOk());
+
+ TableIdentifier table2{.ns = ns, .name = "table2"};
+ create_result = catalog->CreateTable(table2, schema, partition_spec,
sort_order, "",
+ table_properties);
+ ASSERT_THAT(create_result, IsOk());
+
+ // List and varify tables
+ list_result = catalog->ListTables(ns);
+ ASSERT_THAT(list_result, IsOk());
+ EXPECT_THAT(list_result.value(), testing::UnorderedElementsAre(
+ testing::Field(&TableIdentifier::name,
"table1"),
+ testing::Field(&TableIdentifier::name,
"table2")));
+}
+
TEST_F(RestCatalogIntegrationTest, LoadTable) {
auto catalog_result = CreateCatalog();
ASSERT_THAT(catalog_result, IsOk());
@@ -484,4 +526,117 @@ TEST_F(RestCatalogIntegrationTest, DropTable) {
EXPECT_FALSE(load_result.value());
}
+TEST_F(RestCatalogIntegrationTest, RenameTable) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace
+ Namespace ns{.levels = {"test_rename_table"}};
+ auto status = catalog->CreateNamespace(ns, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Create table
+ auto schema = CreateDefaultSchema();
+ auto partition_spec = PartitionSpec::Unpartitioned();
+ auto sort_order = SortOrder::Unsorted();
+
+ TableIdentifier old_table_id{.ns = ns, .name = "old_table"};
+ std::unordered_map<std::string, std::string> table_properties;
+ auto table_result = catalog->CreateTable(old_table_id, schema,
partition_spec,
+ sort_order, "", table_properties);
+ ASSERT_THAT(table_result, IsOk());
+
+ // Rename table
+ TableIdentifier new_table_id{.ns = ns, .name = "new_table"};
+ status = catalog->RenameTable(old_table_id, new_table_id);
+ ASSERT_THAT(status, IsOk());
+
+ // Verify old table no longer exists
+ auto exists_result = catalog->TableExists(old_table_id);
+ ASSERT_THAT(exists_result, IsOk());
+ EXPECT_FALSE(exists_result.value());
+
+ // Verify new table exists
+ exists_result = catalog->TableExists(new_table_id);
+ ASSERT_THAT(exists_result, IsOk());
+ EXPECT_TRUE(exists_result.value());
+}
+
+TEST_F(RestCatalogIntegrationTest, UpdateTable) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace
+ Namespace ns{.levels = {"test_update_table"}};
+ auto status = catalog->CreateNamespace(ns, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Create table
+ auto schema = CreateDefaultSchema();
+ auto partition_spec = PartitionSpec::Unpartitioned();
+ auto sort_order = SortOrder::Unsorted();
+
+ TableIdentifier table_id{.ns = ns, .name = "t1"};
+ std::unordered_map<std::string, std::string> table_properties;
+ auto table_result = catalog->CreateTable(table_id, schema, partition_spec,
sort_order,
+ "", table_properties);
+ ASSERT_THAT(table_result, IsOk());
+ auto& table = table_result.value();
+
+ // Update table properties
+ std::vector<std::unique_ptr<TableRequirement>> requirements;
+ requirements.push_back(std::make_unique<table::AssertUUID>(table->uuid()));
+
+ std::vector<std::unique_ptr<TableUpdate>> updates;
+ updates.push_back(std::make_unique<table::SetProperties>(
+ std::unordered_map<std::string, std::string>{{"key1", "value1"}}));
+
+ auto update_result = catalog->UpdateTable(table_id, requirements, updates);
+ ASSERT_THAT(update_result, IsOk());
+ auto& updated_table = update_result.value();
+
+ // Verify the property was set
+ auto& props = updated_table->metadata()->properties.configs();
+ EXPECT_EQ(props.at("key1"), "value1");
+}
+
+TEST_F(RestCatalogIntegrationTest, RegisterTable) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace
+ Namespace ns{.levels = {"test_register_table"}};
+ auto status = catalog->CreateNamespace(ns, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Create table
+ auto schema = CreateDefaultSchema();
+ auto partition_spec = PartitionSpec::Unpartitioned();
+ auto sort_order = SortOrder::Unsorted();
+
+ TableIdentifier table_id{.ns = ns, .name = "t1"};
+ std::unordered_map<std::string, std::string> table_properties;
+ auto table_result = catalog->CreateTable(table_id, schema, partition_spec,
sort_order,
+ "", table_properties);
+ ASSERT_THAT(table_result, IsOk());
+ auto& table = table_result.value();
+ std::string metadata_location(table->metadata_file_location());
+
+ // Drop table (without purge, to keep metadata file)
+ status = catalog->DropTable(table_id, /*purge=*/false);
+ ASSERT_THAT(status, IsOk());
+
+ // Register table with new name
+ TableIdentifier new_table_id{.ns = ns, .name = "t2"};
+ auto register_result = catalog->RegisterTable(new_table_id,
metadata_location);
+ ASSERT_THAT(register_result, IsOk());
+ auto& registered_table = register_result.value();
+
+ EXPECT_EQ(table->metadata_file_location(),
registered_table->metadata_file_location());
+ EXPECT_NE(table->name(), registered_table->name());
+}
+
} // namespace iceberg::rest
diff --git a/src/iceberg/test/rest_json_internal_test.cc
b/src/iceberg/test/rest_json_internal_test.cc
index 60facf43..24c099fc 100644
--- a/src/iceberg/test/rest_json_internal_test.cc
+++ b/src/iceberg/test/rest_json_internal_test.cc
@@ -30,6 +30,8 @@
#include "iceberg/sort_order.h"
#include "iceberg/table_identifier.h"
#include "iceberg/table_metadata.h"
+#include "iceberg/table_requirement.h"
+#include "iceberg/table_update.h"
#include "iceberg/test/matchers.h"
namespace iceberg::rest {
@@ -1178,4 +1180,204 @@ INSTANTIATE_TEST_SUITE_P(
return info.param.test_name;
});
+DECLARE_ROUNDTRIP_TEST(CommitTableRequest)
+
+INSTANTIATE_TEST_SUITE_P(
+ CommitTableRequestCases, CommitTableRequestTest,
+ ::testing::Values(
+ // Full request with identifier, requirements, and updates
+ CommitTableRequestParam{
+ .test_name = "FullRequest",
+ .expected_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[{"type":"assert-table-uuid","uuid":"2cc52516-5e73-41f2-b139-545d41a4e151"},{"type":"assert-create"}],"updates":[{"action":"assign-uuid","uuid":"2cc52516-5e73-41f2-b139-545d41a4e151"},{"action":"set-current-schema","schema-id":23}]})",
+ .model = {.identifier = TableIdentifier{Namespace{{"ns1"}},
"table1"},
+ .requirements = {std::make_shared<table::AssertUUID>(
+
"2cc52516-5e73-41f2-b139-545d41a4e151"),
+
std::make_shared<table::AssertDoesNotExist>()},
+ .updates = {std::make_shared<table::AssignUUID>(
+ "2cc52516-5e73-41f2-b139-545d41a4e151"),
+
std::make_shared<table::SetCurrentSchema>(23)}}},
+ // Request without identifier (identifier optional)
+ CommitTableRequestParam{
+ .test_name = "WithoutIdentifier",
+ .expected_json_str =
+
R"({"requirements":[{"type":"assert-table-uuid","uuid":"2cc52516-5e73-41f2-b139-545d41a4e151"},{"type":"assert-create"}],"updates":[{"action":"assign-uuid","uuid":"2cc52516-5e73-41f2-b139-545d41a4e151"},{"action":"set-current-schema","schema-id":23}]})",
+ .model = {.requirements = {std::make_shared<table::AssertUUID>(
+
"2cc52516-5e73-41f2-b139-545d41a4e151"),
+
std::make_shared<table::AssertDoesNotExist>()},
+ .updates = {std::make_shared<table::AssignUUID>(
+ "2cc52516-5e73-41f2-b139-545d41a4e151"),
+
std::make_shared<table::SetCurrentSchema>(23)}}},
+ // Request with empty requirements and updates
+ CommitTableRequestParam{
+ .test_name = "EmptyRequirementsAndUpdates",
+ .expected_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[],"updates":[]})",
+ .model = {.identifier = TableIdentifier{Namespace{{"ns1"}},
"table1"}}}),
+ [](const ::testing::TestParamInfo<CommitTableRequestParam>& info) {
+ return info.param.test_name;
+ });
+
+DECLARE_DESERIALIZE_TEST(CommitTableRequest)
+
+INSTANTIATE_TEST_SUITE_P(
+ CommitTableRequestDeserializeCases, CommitTableRequestDeserializeTest,
+ ::testing::Values(
+ // Identifier field is missing (should deserialize to empty identifier)
+ CommitTableRequestDeserializeParam{
+ .test_name = "MissingIdentifier",
+ .json_str = R"({"requirements":[],"updates":[]})",
+ .expected_model = {}}),
+ [](const ::testing::TestParamInfo<CommitTableRequestDeserializeParam>&
info) {
+ return info.param.test_name;
+ });
+
+DECLARE_INVALID_TEST(CommitTableRequest)
+
+INSTANTIATE_TEST_SUITE_P(
+ CommitTableRequestInvalidCases, CommitTableRequestInvalidTest,
+ ::testing::Values(
+ // Invalid table identifier - missing name field
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidTableIdentifier",
+ .invalid_json_str =
R"({"identifier":{},"requirements":[],"updates":[]})",
+ .expected_error_message = "Missing 'name'"},
+ // Invalid table identifier - wrong type for name
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidIdentifierNameType",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":23},"requirements":[],"updates":[]})",
+ .expected_error_message = "type must be string, but is number"},
+ // Invalid requirements - non-object value in requirements array
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidRequirementsNonObject",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[23],"updates":[]})",
+ .expected_error_message = "Missing 'type' in"},
+ // Invalid requirements - missing type field
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidRequirementsMissingType",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[{}],"updates":[]})",
+ .expected_error_message = "Missing 'type'"},
+ // Invalid requirements - assert-table-uuid missing uuid field
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidRequirementsMissingUUID",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[{"type":"assert-table-uuid"}],"updates":[]})",
+ .expected_error_message = "Missing 'uuid'"},
+ // Invalid updates - non-object value in updates array
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidUpdatesNonObject",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[],"updates":[23]})",
+ .expected_error_message = "Missing 'action' in"},
+ // Invalid updates - missing action field
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidUpdatesMissingAction",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[],"updates":[{}]})",
+ .expected_error_message = "Missing 'action'"},
+ // Invalid updates - assign-uuid missing uuid field
+ CommitTableRequestInvalidParam{
+ .test_name = "InvalidUpdatesMissingUUID",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[],"updates":[{"action":"assign-uuid"}]})",
+ .expected_error_message = "Missing 'uuid'"},
+ // Missing required requirements field
+ CommitTableRequestInvalidParam{
+ .test_name = "MissingRequirements",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"updates":[]})",
+ .expected_error_message = "Missing 'requirements'"},
+ // Missing required updates field
+ CommitTableRequestInvalidParam{
+ .test_name = "MissingUpdates",
+ .invalid_json_str =
+
R"({"identifier":{"namespace":["ns1"],"name":"table1"},"requirements":[]})",
+ .expected_error_message = "Missing 'updates'"},
+ // Empty JSON object
+ CommitTableRequestInvalidParam{
+ .test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing 'requirements'"}),
+ [](const ::testing::TestParamInfo<CommitTableRequestInvalidParam>& info) {
+ return info.param.test_name;
+ });
+
+DECLARE_ROUNDTRIP_TEST(CommitTableResponse)
+
+INSTANTIATE_TEST_SUITE_P(
+ CommitTableResponseCases, CommitTableResponseTest,
+ ::testing::Values(
+ // Full response with metadata location and metadata
+ CommitTableResponseParam{
+ .test_name = "FullResponse",
+ .expected_json_str =
+
R"({"metadata-location":"s3://bucket/metadata/v2.json","metadata":{"current-schema-id":1,"current-snapshot-id":null,"default-sort-order-id":0,"default-spec-id":0,"format-version":2,"last-column-id":1,"last-partition-id":0,"last-sequence-number":0,"last-updated-ms":0,"location":"s3://bucket/test","metadata-log":[],"partition-specs":[{"fields":[],"spec-id":0}],"partition-statistics":[],"properties":{},"refs":{},"schemas":[{"fields":[{"id":1,"name":"id","required":true,"type
[...]
+ .model = {.metadata_location = "s3://bucket/metadata/v2.json",
+ .metadata = MakeSimpleTableMetadata()}}),
+ [](const ::testing::TestParamInfo<CommitTableResponseParam>& info) {
+ return info.param.test_name;
+ });
+
+DECLARE_DESERIALIZE_TEST(CommitTableResponse)
+
+INSTANTIATE_TEST_SUITE_P(
+ CommitTableResponseDeserializeCases, CommitTableResponseDeserializeTest,
+ ::testing::Values(
+ // Standard response with all fields
+ CommitTableResponseDeserializeParam{
+ .test_name = "StandardResponse",
+ .json_str =
+
R"({"metadata-location":"s3://bucket/metadata/v2.json","metadata":{"format-version":2,"table-uuid":"test-uuid-1234","location":"s3://bucket/test","last-sequence-number":0,"last-updated-ms":0,"last-column-id":1,"schemas":[{"type":"struct","schema-id":1,"fields":[{"id":1,"name":"id","type":"int","required":true}]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"default
[...]
+ .expected_model = {.metadata_location =
"s3://bucket/metadata/v2.json",
+ .metadata = MakeSimpleTableMetadata()}}),
+ [](const ::testing::TestParamInfo<CommitTableResponseDeserializeParam>&
info) {
+ return info.param.test_name;
+ });
+
+DECLARE_INVALID_TEST(CommitTableResponse)
+
+INSTANTIATE_TEST_SUITE_P(
+ CommitTableResponseInvalidCases, CommitTableResponseInvalidTest,
+ ::testing::Values(
+ // Missing required metadata-location field
+ CommitTableResponseInvalidParam{
+ .test_name = "MissingMetadataLocation",
+ .invalid_json_str =
+
R"({"metadata":{"format-version":2,"table-uuid":"test","location":"s3://test","last-sequence-number":0,"last-column-id":1,"last-updated-ms":0,"schemas":[{"type":"struct","schema-id":1,"fields":[{"id":1,"name":"id","type":"int","required":true}]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0}})",
+ .expected_error_message = "Missing 'metadata-location'"},
+ // Missing required metadata field
+ CommitTableResponseInvalidParam{
+ .test_name = "MissingMetadata",
+ .invalid_json_str =
R"({"metadata-location":"s3://bucket/metadata/v2.json"})",
+ .expected_error_message = "Missing 'metadata'"},
+ // Null metadata field
+ CommitTableResponseInvalidParam{
+ .test_name = "NullMetadata",
+ .invalid_json_str =
+
R"({"metadata-location":"s3://bucket/metadata/v2.json","metadata":null})",
+ .expected_error_message = "Missing 'metadata'"},
+ // Wrong type for metadata-location field
+ CommitTableResponseInvalidParam{
+ .test_name = "WrongMetadataLocationType",
+ .invalid_json_str =
+
R"({"metadata-location":123,"metadata":{"format-version":2,"table-uuid":"test","location":"s3://test","last-sequence-number":0,"last-column-id":1,"last-updated-ms":0,"schemas":[{"type":"struct","schema-id":1,"fields":[{"id":1,"name":"id","type":"int","required":true}]}],"current-schema-id":1,"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0}})",
+ .expected_error_message = "type must be string, but is number"},
+ // Wrong type for metadata field
+ CommitTableResponseInvalidParam{
+ .test_name = "WrongMetadataType",
+ .invalid_json_str =
+
R"({"metadata-location":"s3://bucket/metadata/v2.json","metadata":"invalid"})",
+ .expected_error_message = "Cannot parse metadata from a
non-object"},
+ // Empty JSON object
+ CommitTableResponseInvalidParam{
+ .test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing 'metadata-location'"}),
+ [](const ::testing::TestParamInfo<CommitTableResponseInvalidParam>& info) {
+ return info.param.test_name;
+ });
+
} // namespace iceberg::rest