Skip to content

Commit 499c8df

Browse files
committed
feat: implement REST catalog namespace operations
1 parent 428a171 commit 499c8df

File tree

7 files changed

+295
-25
lines changed

7 files changed

+295
-25
lines changed

src/iceberg/catalog/rest/http_client.cc

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "iceberg/catalog/rest/constant.h"
2626
#include "iceberg/catalog/rest/error_handlers.h"
2727
#include "iceberg/catalog/rest/json_internal.h"
28+
#include "iceberg/catalog/rest/rest_util.h"
2829
#include "iceberg/json_internal.h"
2930
#include "iceberg/result.h"
3031
#include "iceberg/util/macros.h"
@@ -63,6 +64,9 @@ std::unordered_map<std::string, std::string> HttpResponse::headers() const {
6364

6465
namespace {
6566

67+
/// \brief Default error type for unparseable REST responses.
68+
constexpr std::string_view kRestExceptionType = "RESTException";
69+
6670
/// \brief Merges global default headers with request-specific headers.
6771
///
6872
/// Combines the global headers derived from RestCatalogProperties with the headers
@@ -96,16 +100,37 @@ bool IsSuccessful(int32_t status_code) {
96100
|| status_code == 304; // Not Modified
97101
}
98102

103+
/// \brief Builds a default ErrorResponse when the response body cannot be parsed.
104+
ErrorResponse BuildDefaultErrorResponse(const cpr::Response& response) {
105+
return {
106+
.code = static_cast<uint32_t>(response.status_code),
107+
.type = std::string(kRestExceptionType),
108+
.message = !response.reason.empty() ? response.reason
109+
: GetStandardReasonPhrase(response.status_code),
110+
};
111+
}
112+
113+
/// \brief Tries to parse the response body as an ErrorResponse.
114+
Result<ErrorResponse> TryParseErrorResponse(const std::string& text) {
115+
if (text.empty()) {
116+
return ErrorResponse();
117+
}
118+
ICEBERG_ASSIGN_OR_RAISE(auto json_result, FromJsonString(text));
119+
ICEBERG_ASSIGN_OR_RAISE(auto error_result, ErrorResponseFromJson(json_result));
120+
return error_result;
121+
}
122+
99123
/// \brief Handles failure responses by invoking the provided error handler.
100124
Status HandleFailureResponse(const cpr::Response& response,
101125
const ErrorHandler& error_handler) {
102-
if (!IsSuccessful(response.status_code)) {
103-
// TODO(gangwu): response status code is lost, wrap it with RestError.
104-
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.text));
105-
ICEBERG_ASSIGN_OR_RAISE(auto error_response, ErrorResponseFromJson(json));
106-
return error_handler.Accept(error_response);
126+
if (IsSuccessful(response.status_code)) {
127+
return {};
107128
}
108-
return {};
129+
auto parse_result = TryParseErrorResponse(response.text);
130+
const ErrorResponse final_error = parse_result.has_value()
131+
? std::move(*parse_result)
132+
: BuildDefaultErrorResponse(response);
133+
return error_handler.Accept(final_error);
109134
}
110135

111136
} // namespace

src/iceberg/catalog/rest/json_internal.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ Result<LoadTableResult> LoadTableResultFromJson(const nlohmann::json& json) {
213213
ICEBERG_ASSIGN_OR_RAISE(result.metadata, TableMetadataFromJson(metadata_json));
214214
ICEBERG_ASSIGN_OR_RAISE(result.config,
215215
GetJsonValueOrDefault<decltype(result.config)>(json, kConfig));
216+
ICEBERG_RETURN_UNEXPECTED(result.Validate());
216217
return result;
217218
}
218219

@@ -257,6 +258,7 @@ Result<CreateNamespaceResponse> CreateNamespaceResponseFromJson(
257258
ICEBERG_ASSIGN_OR_RAISE(
258259
response.properties,
259260
GetJsonValueOrDefault<decltype(response.properties)>(json, kProperties));
261+
ICEBERG_RETURN_UNEXPECTED(response.Validate());
260262
return response;
261263
}
262264

@@ -274,6 +276,7 @@ Result<GetNamespaceResponse> GetNamespaceResponseFromJson(const nlohmann::json&
274276
ICEBERG_ASSIGN_OR_RAISE(
275277
response.properties,
276278
GetJsonValueOrDefault<decltype(response.properties)>(json, kProperties));
279+
ICEBERG_RETURN_UNEXPECTED(response.Validate());
277280
return response;
278281
}
279282

src/iceberg/catalog/rest/rest_catalog.cc

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
#include "iceberg/catalog/rest/rest_catalog.h"
3535
#include "iceberg/catalog/rest/rest_util.h"
3636
#include "iceberg/json_internal.h"
37+
#include "iceberg/partition_spec.h"
3738
#include "iceberg/result.h"
39+
#include "iceberg/schema.h"
3840
#include "iceberg/table.h"
3941
#include "iceberg/util/macros.h"
4042

@@ -115,29 +117,66 @@ Result<std::vector<Namespace>> RestCatalog::ListNamespaces(const Namespace& ns)
115117
}
116118

117119
Status RestCatalog::CreateNamespace(
118-
[[maybe_unused]] const Namespace& ns,
119-
[[maybe_unused]] const std::unordered_map<std::string, std::string>& properties) {
120-
return NotImplemented("Not implemented");
120+
const Namespace& ns, const std::unordered_map<std::string, std::string>& properties) {
121+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespaces());
122+
CreateNamespaceRequest request{.namespace_ = ns, .properties = properties};
123+
ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
124+
ICEBERG_ASSIGN_OR_RAISE(const auto& response,
125+
client_->Post(endpoint, json_request, /*headers=*/{},
126+
*NamespaceErrorHandler::Instance()));
127+
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
128+
ICEBERG_ASSIGN_OR_RAISE(auto create_response, CreateNamespaceResponseFromJson(json));
129+
return {};
121130
}
122131

123132
Result<std::unordered_map<std::string, std::string>> RestCatalog::GetNamespaceProperties(
124-
[[maybe_unused]] const Namespace& ns) const {
125-
return NotImplemented("Not implemented");
126-
}
127-
128-
Status RestCatalog::DropNamespace([[maybe_unused]] const Namespace& ns) {
129-
return NotImplemented("Not implemented");
130-
}
131-
132-
Result<bool> RestCatalog::NamespaceExists([[maybe_unused]] const Namespace& ns) const {
133-
return NotImplemented("Not implemented");
133+
const Namespace& ns) const {
134+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
135+
ICEBERG_ASSIGN_OR_RAISE(const auto& response,
136+
client_->Get(endpoint, /*params=*/{}, /*headers=*/{},
137+
*NamespaceErrorHandler::Instance()));
138+
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
139+
ICEBERG_ASSIGN_OR_RAISE(auto get_response, GetNamespaceResponseFromJson(json));
140+
return get_response.properties;
141+
}
142+
143+
Status RestCatalog::DropNamespace(const Namespace& ns) {
144+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
145+
auto response_or_error =
146+
client_->Delete(endpoint, /*headers=*/{}, *DropNamespaceErrorHandler::Instance());
147+
return {};
148+
}
149+
150+
Result<bool> RestCatalog::NamespaceExists(const Namespace& ns) const {
151+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
152+
auto response_or_error =
153+
client_->Head(endpoint, /*headers=*/{}, *NamespaceErrorHandler::Instance());
154+
if (!response_or_error.has_value()) {
155+
const auto& error = response_or_error.error();
156+
// catch NoSuchNamespaceException/404 and return false
157+
if (error.kind == ErrorKind::kNoSuchNamespace) {
158+
return false;
159+
}
160+
ICEBERG_RETURN_UNEXPECTED(response_or_error);
161+
}
162+
return true;
134163
}
135164

136165
Status RestCatalog::UpdateNamespaceProperties(
137-
[[maybe_unused]] const Namespace& ns,
138-
[[maybe_unused]] const std::unordered_map<std::string, std::string>& updates,
139-
[[maybe_unused]] const std::unordered_set<std::string>& removals) {
140-
return NotImplemented("Not implemented");
166+
const Namespace& ns, const std::unordered_map<std::string, std::string>& updates,
167+
const std::unordered_set<std::string>& removals) {
168+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->NamespaceProperties(ns));
169+
UpdateNamespacePropertiesRequest request{
170+
.removals = std::vector<std::string>(removals.begin(), removals.end()),
171+
.updates = updates};
172+
ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
173+
ICEBERG_ASSIGN_OR_RAISE(const auto& response,
174+
client_->Post(endpoint, json_request, /*headers=*/{},
175+
*NamespaceErrorHandler::Instance()));
176+
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
177+
ICEBERG_ASSIGN_OR_RAISE(auto update_response,
178+
UpdateNamespacePropertiesResponseFromJson(json));
179+
return {};
141180
}
142181

143182
Result<std::vector<TableIdentifier>> RestCatalog::ListTables(

src/iceberg/catalog/rest/rest_util.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
#include "iceberg/catalog/rest/rest_util.h"
2121

22+
#include <format>
23+
2224
#include <cpr/util.h>
2325

2426
#include "iceberg/table_identifier.h"
@@ -120,4 +122,45 @@ std::unordered_map<std::string, std::string> MergeConfigs(
120122
return merged;
121123
}
122124

125+
std::string GetStandardReasonPhrase(int32_t status_code) {
126+
switch (status_code) {
127+
case 200:
128+
return "OK";
129+
case 201:
130+
return "Created";
131+
case 202:
132+
return "Accepted";
133+
case 204:
134+
return "No Content";
135+
case 400:
136+
return "Bad Request";
137+
case 401:
138+
return "Unauthorized";
139+
case 403:
140+
return "Forbidden";
141+
case 404:
142+
return "Not Found";
143+
case 405:
144+
return "Method Not Allowed";
145+
case 406:
146+
return "Not Acceptable";
147+
case 409:
148+
return "Conflict";
149+
case 422:
150+
return "Unprocessable Entity";
151+
case 500:
152+
return "Internal Server Error";
153+
case 501:
154+
return "Not Implemented";
155+
case 502:
156+
return "Bad Gateway";
157+
case 503:
158+
return "Service Unavailable";
159+
case 504:
160+
return "Gateway Timeout";
161+
default:
162+
return std::format("HTTP {}", status_code);
163+
}
164+
}
165+
123166
} // namespace iceberg::rest

src/iceberg/catalog/rest/rest_util.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,13 @@ ICEBERG_REST_EXPORT std::unordered_map<std::string, std::string> MergeConfigs(
8181
const std::unordered_map<std::string, std::string>& client_configs,
8282
const std::unordered_map<std::string, std::string>& server_overrides);
8383

84+
/// \brief Get the standard HTTP reason phrase for a status code.
85+
///
86+
/// \details Returns the standard English reason phrase for common HTTP status codes.
87+
/// For unknown status codes, returns a generic "HTTP {code}" message.
88+
/// \param status_code The HTTP status code (e.g., 200, 404, 500).
89+
/// \return The standard reason phrase string (e.g., "OK", "Not Found", "Internal Server
90+
/// Error").
91+
ICEBERG_REST_EXPORT std::string GetStandardReasonPhrase(int32_t status_code);
92+
8493
} // namespace iceberg::rest

src/iceberg/catalog/rest/types.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ struct ICEBERG_REST_EXPORT CatalogConfig {
5353

5454
/// \brief JSON error payload returned in a response with further details on the error.
5555
struct ICEBERG_REST_EXPORT ErrorResponse {
56-
std::string message; // required
57-
std::string type; // required
5856
uint32_t code; // required
57+
std::string type; // required
58+
std::string message; // required
5959
std::vector<std::string> stack;
6060

6161
/// \brief Validates the ErrorResponse.

0 commit comments

Comments
 (0)