Skip to content

Commit f93f54d

Browse files
authored
feat: implement REST catalog namespace operations (#404)
1 parent 36f746c commit f93f54d

File tree

8 files changed

+394
-34
lines changed

8 files changed

+394
-34
lines changed

src/iceberg/catalog/rest/http_client.cc

Lines changed: 30 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,36 @@ 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 InvalidArgument("Empty response body");
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 =
131+
parse_result.value_or(BuildDefaultErrorResponse(response));
132+
return error_handler.Accept(final_error);
109133
}
110134

111135
} // 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: 58 additions & 16 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

@@ -99,7 +101,7 @@ Result<std::vector<Namespace>> RestCatalog::ListNamespaces(const Namespace& ns)
99101
if (!next_token.empty()) {
100102
params[kQueryParamPageToken] = next_token;
101103
}
102-
ICEBERG_ASSIGN_OR_RAISE(const auto& response,
104+
ICEBERG_ASSIGN_OR_RAISE(const auto response,
103105
client_->Get(endpoint, params, /*headers=*/{},
104106
*NamespaceErrorHandler::Instance()));
105107
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
@@ -115,29 +117,69 @@ 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");
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;
130141
}
131142

132-
Result<bool> RestCatalog::NamespaceExists([[maybe_unused]] const Namespace& ns) const {
133-
return NotImplemented("Not implemented");
143+
Status RestCatalog::DropNamespace(const Namespace& ns) {
144+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
145+
ICEBERG_ASSIGN_OR_RAISE(
146+
const auto response,
147+
client_->Delete(endpoint, /*headers=*/{}, *DropNamespaceErrorHandler::Instance()));
148+
return {};
149+
}
150+
151+
Result<bool> RestCatalog::NamespaceExists(const Namespace& ns) const {
152+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
153+
// TODO(Feiyang Li): checks if the server supports the namespace exists endpoint, if
154+
// not, triggers a fallback mechanism
155+
auto response_or_error =
156+
client_->Head(endpoint, /*headers=*/{}, *NamespaceErrorHandler::Instance());
157+
if (!response_or_error.has_value()) {
158+
const auto& error = response_or_error.error();
159+
// catch NoSuchNamespaceException/404 and return false
160+
if (error.kind == ErrorKind::kNoSuchNamespace) {
161+
return false;
162+
}
163+
ICEBERG_RETURN_UNEXPECTED(response_or_error);
164+
}
165+
return true;
134166
}
135167

136168
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");
169+
const Namespace& ns, const std::unordered_map<std::string, std::string>& updates,
170+
const std::unordered_set<std::string>& removals) {
171+
ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->NamespaceProperties(ns));
172+
UpdateNamespacePropertiesRequest request{
173+
.removals = std::vector<std::string>(removals.begin(), removals.end()),
174+
.updates = updates};
175+
ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
176+
ICEBERG_ASSIGN_OR_RAISE(const auto response,
177+
client_->Post(endpoint, json_request, /*headers=*/{},
178+
*NamespaceErrorHandler::Instance()));
179+
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
180+
ICEBERG_ASSIGN_OR_RAISE(auto update_response,
181+
UpdateNamespacePropertiesResponseFromJson(json));
182+
return {};
141183
}
142184

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

src/iceberg/catalog/rest/rest_util.cc

Lines changed: 131 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,133 @@ 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 100:
128+
return "Continue";
129+
case 101:
130+
return "Switching Protocols";
131+
case 102:
132+
return "Processing";
133+
case 103:
134+
return "Early Hints";
135+
case 200:
136+
return "OK";
137+
case 201:
138+
return "Created";
139+
case 202:
140+
return "Accepted";
141+
case 203:
142+
return "Non Authoritative Information";
143+
case 204:
144+
return "No Content";
145+
case 205:
146+
return "Reset Content";
147+
case 206:
148+
return "Partial Content";
149+
case 207:
150+
return "Multi-Status";
151+
case 208:
152+
return "Already Reported";
153+
case 226:
154+
return "IM Used";
155+
case 300:
156+
return "Multiple Choices";
157+
case 301:
158+
return "Moved Permanently";
159+
case 302:
160+
return "Moved Temporarily";
161+
case 303:
162+
return "See Other";
163+
case 304:
164+
return "Not Modified";
165+
case 305:
166+
return "Use Proxy";
167+
case 307:
168+
return "Temporary Redirect";
169+
case 308:
170+
return "Permanent Redirect";
171+
case 400:
172+
return "Bad Request";
173+
case 401:
174+
return "Unauthorized";
175+
case 402:
176+
return "Payment Required";
177+
case 403:
178+
return "Forbidden";
179+
case 404:
180+
return "Not Found";
181+
case 405:
182+
return "Method Not Allowed";
183+
case 406:
184+
return "Not Acceptable";
185+
case 407:
186+
return "Proxy Authentication Required";
187+
case 408:
188+
return "Request Timeout";
189+
case 409:
190+
return "Conflict";
191+
case 410:
192+
return "Gone";
193+
case 411:
194+
return "Length Required";
195+
case 412:
196+
return "Precondition Failed";
197+
case 413:
198+
return "Request Too Long";
199+
case 414:
200+
return "Request-URI Too Long";
201+
case 415:
202+
return "Unsupported Media Type";
203+
case 416:
204+
return "Requested Range Not Satisfiable";
205+
case 417:
206+
return "Expectation Failed";
207+
case 421:
208+
return "Misdirected Request";
209+
case 422:
210+
return "Unprocessable Content";
211+
case 423:
212+
return "Locked";
213+
case 424:
214+
return "Failed Dependency";
215+
case 425:
216+
return "Too Early";
217+
case 426:
218+
return "Upgrade Required";
219+
case 428:
220+
return "Precondition Required";
221+
case 429:
222+
return "Too Many Requests";
223+
case 431:
224+
return "Request Header Fields Too Large";
225+
case 451:
226+
return "Unavailable For Legal Reasons";
227+
case 500:
228+
return "Internal Server Error";
229+
case 501:
230+
return "Not Implemented";
231+
case 502:
232+
return "Bad Gateway";
233+
case 503:
234+
return "Service Unavailable";
235+
case 504:
236+
return "Gateway Timeout";
237+
case 505:
238+
return "Http Version Not Supported";
239+
case 506:
240+
return "Variant Also Negotiates";
241+
case 507:
242+
return "Insufficient Storage";
243+
case 508:
244+
return "Loop Detected";
245+
case 510:
246+
return "Not Extended";
247+
case 511:
248+
return "Network Authentication Required";
249+
default:
250+
return std::format("HTTP {}", status_code);
251+
}
252+
}
253+
123254
} // 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)