Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/iceberg/catalog/rest/catalog_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class ICEBERG_REST_EXPORT RestCatalogProperties
inline static Entry<std::string> kWarehouse{"warehouse", ""};
/// \brief The optional prefix for REST API paths.
inline static Entry<std::string> kPrefix{"prefix", ""};
/// \brief The encoded separator used to join namespace levels in REST paths.
inline static Entry<std::string> kNamespaceSeparator{"namespace-separator", "%1F"};
/// \brief The snapshot loading mode (ALL or REFS).
inline static Entry<std::string> kSnapshotLoadingMode{"snapshot-loading-mode", "ALL"};
/// \brief The prefix for HTTP headers.
Expand Down
36 changes: 24 additions & 12 deletions src/iceberg/catalog/rest/resource_paths.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@

namespace iceberg::rest {

Result<std::unique_ptr<ResourcePaths>> ResourcePaths::Make(std::string base_uri,
const std::string& prefix) {
Result<std::unique_ptr<ResourcePaths>> ResourcePaths::Make(
std::string base_uri, const std::string& prefix,
const std::string& namespace_separator) {
if (base_uri.empty()) {
return InvalidArgument("Base URI is empty");
}
return std::unique_ptr<ResourcePaths>(new ResourcePaths(std::move(base_uri), prefix));
return std::unique_ptr<ResourcePaths>(
new ResourcePaths(std::move(base_uri), prefix, namespace_separator));
}

ResourcePaths::ResourcePaths(std::string base_uri, const std::string& prefix)
: base_uri_(std::move(base_uri)), prefix_(prefix.empty() ? "" : (prefix + "/")) {}
ResourcePaths::ResourcePaths(std::string base_uri, const std::string& prefix,
std::string namespace_separator)
: base_uri_(std::move(base_uri)),
prefix_(prefix.empty() ? "" : (prefix + "/")),
namespace_separator_(std::move(namespace_separator)) {}

Result<std::string> ResourcePaths::Config() const {
return std::format("{}/v1/config", base_uri_);
Expand All @@ -51,31 +56,36 @@ Result<std::string> ResourcePaths::Namespaces() const {
}

Result<std::string> ResourcePaths::Namespace_(const Namespace& ns) const {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace, EncodeNamespace(ns));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace,
EncodeNamespace(ns, namespace_separator_));
return std::format("{}/v1/{}namespaces/{}", base_uri_, prefix_, encoded_namespace);
}

Result<std::string> ResourcePaths::NamespaceProperties(const Namespace& ns) const {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace, EncodeNamespace(ns));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace,
EncodeNamespace(ns, namespace_separator_));
return std::format("{}/v1/{}namespaces/{}/properties", base_uri_, prefix_,
encoded_namespace);
}

Result<std::string> ResourcePaths::Tables(const Namespace& ns) const {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace, EncodeNamespace(ns));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace,
EncodeNamespace(ns, namespace_separator_));
return std::format("{}/v1/{}namespaces/{}/tables", base_uri_, prefix_,
encoded_namespace);
}

Result<std::string> ResourcePaths::Table(const TableIdentifier& ident) const {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace, EncodeNamespace(ident.ns));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace,
EncodeNamespace(ident.ns, namespace_separator_));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_table_name, EncodeString(ident.name));
return std::format("{}/v1/{}namespaces/{}/tables/{}", base_uri_, prefix_,
encoded_namespace, encoded_table_name);
}

Result<std::string> ResourcePaths::Register(const Namespace& ns) const {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace, EncodeNamespace(ns));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace,
EncodeNamespace(ns, namespace_separator_));
return std::format("{}/v1/{}namespaces/{}/register", base_uri_, prefix_,
encoded_namespace);
}
Expand All @@ -85,14 +95,16 @@ Result<std::string> ResourcePaths::Rename() const {
}

Result<std::string> ResourcePaths::Metrics(const TableIdentifier& ident) const {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace, EncodeNamespace(ident.ns));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace,
EncodeNamespace(ident.ns, namespace_separator_));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_table_name, EncodeString(ident.name));
return std::format("{}/v1/{}namespaces/{}/tables/{}/metrics", base_uri_, prefix_,
encoded_namespace, encoded_table_name);
}

Result<std::string> ResourcePaths::Credentials(const TableIdentifier& ident) const {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace, EncodeNamespace(ident.ns));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_namespace,
EncodeNamespace(ident.ns, namespace_separator_));
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_table_name, EncodeString(ident.name));
return std::format("{}/v1/{}namespaces/{}/tables/{}/credentials", base_uri_, prefix_,
encoded_namespace, encoded_table_name);
Expand Down
10 changes: 7 additions & 3 deletions src/iceberg/catalog/rest/resource_paths.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ class ICEBERG_REST_EXPORT ResourcePaths {
/// \brief Construct a ResourcePaths with base URI and optional prefix.
/// \param base_uri The base URI of the REST catalog server (without trailing slash)
/// \param prefix Optional prefix for REST API paths (default: empty)
/// \param namespace_separator Encoded separator used between namespace levels.
/// \return A unique_ptr to ResourcePaths instance
static Result<std::unique_ptr<ResourcePaths>> Make(std::string base_uri,
const std::string& prefix);
static Result<std::unique_ptr<ResourcePaths>> Make(
std::string base_uri, const std::string& prefix,
const std::string& namespace_separator);

/// \brief Get the /v1/config endpoint path.
Result<std::string> Config() const;
Expand Down Expand Up @@ -82,10 +84,12 @@ class ICEBERG_REST_EXPORT ResourcePaths {
Result<std::string> CommitTransaction() const;

private:
ResourcePaths(std::string base_uri, const std::string& prefix);
ResourcePaths(std::string base_uri, const std::string& prefix,
std::string namespace_separator);

std::string base_uri_; // required
const std::string prefix_; // optional
const std::string namespace_separator_;
};

} // namespace iceberg::rest
16 changes: 11 additions & 5 deletions src/iceberg/catalog/rest/rest_catalog.cc
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ Result<std::shared_ptr<RestCatalog>> RestCatalog::Make(
ICEBERG_ASSIGN_OR_RAISE(auto auth_manager,
auth::AuthManagers::Load(catalog_name, config.configs()));
ICEBERG_ASSIGN_OR_RAISE(
auto paths, ResourcePaths::Make(std::string(TrimTrailingSlash(uri)),
config.Get(RestCatalogProperties::kPrefix)));
auto paths,
ResourcePaths::Make(std::string(TrimTrailingSlash(uri)),
config.Get(RestCatalogProperties::kPrefix),
config.Get(RestCatalogProperties::kNamespaceSeparator)));

// Create init session for fetching server configuration
HttpClient init_client(config.ExtractHeaders());
Expand All @@ -158,8 +160,10 @@ Result<std::shared_ptr<RestCatalog>> RestCatalog::Make(
// Update resource paths based on the final config
ICEBERG_ASSIGN_OR_RAISE(auto final_uri, final_config.Uri());
ICEBERG_ASSIGN_OR_RAISE(
paths, ResourcePaths::Make(std::string(TrimTrailingSlash(final_uri)),
final_config.Get(RestCatalogProperties::kPrefix)));
paths,
ResourcePaths::Make(std::string(TrimTrailingSlash(final_uri)),
final_config.Get(RestCatalogProperties::kPrefix),
final_config.Get(RestCatalogProperties::kNamespaceSeparator)));

// Get snapshot loading mode
ICEBERG_ASSIGN_OR_RAISE(auto snapshot_mode, final_config.SnapshotLoadingMode());
Expand Down Expand Up @@ -203,7 +207,9 @@ Result<std::vector<Namespace>> RestCatalog::ListNamespaces(const Namespace& ns)
while (true) {
std::unordered_map<std::string, std::string> params;
if (!ns.levels.empty()) {
ICEBERG_ASSIGN_OR_RAISE(params[kQueryParamParent], EncodeNamespace(ns));
ICEBERG_ASSIGN_OR_RAISE(
params[kQueryParamParent],
EncodeNamespace(ns, config_.Get(RestCatalogProperties::kNamespaceSeparator)));
}
if (!next_token.empty()) {
params[kQueryParamPageToken] = next_token;
Expand Down
18 changes: 8 additions & 10 deletions src/iceberg/catalog/rest/rest_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@

namespace iceberg::rest {

namespace {
const std::string kNamespaceEscapeSeparator = "%1F";
}

std::string_view TrimTrailingSlash(std::string_view str) {
while (!str.empty() && str.back() == '/') {
str.remove_suffix(1);
Expand Down Expand Up @@ -69,7 +65,8 @@ Result<std::string> DecodeString(std::string_view str_to_decode) {
return std::string{decoded.data(), decoded.size()};
}

Result<std::string> EncodeNamespace(const Namespace& ns_to_encode) {
Result<std::string> EncodeNamespace(const Namespace& ns_to_encode,
std::string_view separator) {
if (ns_to_encode.levels.empty()) {
return "";
}
Expand All @@ -79,28 +76,29 @@ Result<std::string> EncodeNamespace(const Namespace& ns_to_encode) {
for (size_t i = 1; i < ns_to_encode.levels.size(); ++i) {
ICEBERG_ASSIGN_OR_RAISE(std::string encoded_level,
EncodeString(ns_to_encode.levels[i]));
result.append(kNamespaceEscapeSeparator);
result.append(separator);
result.append(std::move(encoded_level));
}

return result;
}

Result<Namespace> DecodeNamespace(std::string_view str_to_decode) {
Result<Namespace> DecodeNamespace(std::string_view str_to_decode,
std::string_view separator) {
if (str_to_decode.empty()) {
return Namespace{.levels = {}};
}

Namespace ns{};
std::string::size_type start = 0;
std::string::size_type end = str_to_decode.find(kNamespaceEscapeSeparator);
std::string::size_type end = str_to_decode.find(separator);

while (end != std::string::npos) {
ICEBERG_ASSIGN_OR_RAISE(std::string decoded_level,
DecodeString(str_to_decode.substr(start, end - start)));
ns.levels.push_back(std::move(decoded_level));
start = end + kNamespaceEscapeSeparator.size();
end = str_to_decode.find(kNamespaceEscapeSeparator, start);
start = end + separator.size();
end = str_to_decode.find(separator, start);
}

ICEBERG_ASSIGN_OR_RAISE(std::string decoded_level,
Expand Down
15 changes: 10 additions & 5 deletions src/iceberg/catalog/rest/rest_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,23 @@ ICEBERG_REST_EXPORT Result<std::string> DecodeString(std::string_view str_to_dec

/// \brief Encode a Namespace into a URL-safe component.
///
/// \details Encodes each level separately using EncodeString, then joins them with "%1F".
/// \details Encodes each level separately using EncodeString, then joins them with the
/// provided separator. The default matches the REST spec's historical "%1F".
/// \param ns_to_encode The namespace to encode.
/// \param separator The encoded separator to place between namespace levels.
/// \return The percent-encoded namespace string suitable for URLs.
ICEBERG_REST_EXPORT Result<std::string> EncodeNamespace(const Namespace& ns_to_encode);
ICEBERG_REST_EXPORT Result<std::string> EncodeNamespace(
const Namespace& ns_to_encode, std::string_view separator = "%1F");

/// \brief Decode a URL-encoded namespace string back to a Namespace.
///
/// \details Splits by "%1F" (the URL-encoded form of ASCII Unit Separator), then decodes
/// each level separately using DecodeString.
/// \details Splits by the provided separator, then decodes each level separately using
/// DecodeString.
/// \param str_to_decode The percent-encoded namespace string.
/// \param separator The encoded separator used between namespace levels.
/// \return The decoded Namespace.
ICEBERG_REST_EXPORT Result<Namespace> DecodeNamespace(std::string_view str_to_decode);
ICEBERG_REST_EXPORT Result<Namespace> DecodeNamespace(std::string_view str_to_decode,
std::string_view separator = "%1F");

/// \brief Merge catalog configuration properties.
///
Expand Down
9 changes: 9 additions & 0 deletions src/iceberg/test/rest_util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ TEST(RestUtilTest, RoundTripUrlEncodeDecodeNamespace) {
EXPECT_THAT(DecodeNamespace(""), HasValue(::testing::Eq(Namespace{.levels = {}})));
}

TEST(RestUtilTest, RoundTripNamespaceWithCustomSeparator) {
Namespace ns{.levels = {"dogs.and.cats", "named", "hank.or.james-westfall"}};

EXPECT_THAT(EncodeNamespace(ns, "%2E"),
HasValue(::testing::Eq("dogs.and.cats%2Enamed%2Ehank.or.james-westfall")));
EXPECT_THAT(DecodeNamespace("dogs.and.cats%2Enamed%2Ehank.or.james-westfall", "%2E"),
HasValue(::testing::Eq(ns)));
}

TEST(RestUtilTest, EncodeString) {
// RFC 3986 unreserved characters should not be encoded
EXPECT_THAT(EncodeString("abc123XYZ"), HasValue(::testing::Eq("abc123XYZ")));
Expand Down
Loading