diff --git a/src/Databases/Iceberg/RestCatalog.cpp b/src/Databases/Iceberg/RestCatalog.cpp index 5ea25a663bac..b7111d01b5af 100644 --- a/src/Databases/Iceberg/RestCatalog.cpp +++ b/src/Databases/Iceberg/RestCatalog.cpp @@ -84,6 +84,24 @@ std::string correctAPIURI(const std::string & uri) return std::filesystem::path(uri) / "v1"; } +/// Encode namespace for use in URL path. +/// According to the Iceberg REST Catalog spec, multi-level namespaces +/// should be joined with the unit separator character (0x1F) which is +/// URL-encoded as %1F. +/// https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml +String encodeNamespaceForURI(const String & namespace_name) +{ + String encoded; + for (const auto & ch : namespace_name) + { + if (ch == '.') + encoded += "%1F"; + else + encoded.push_back(ch); + } + return encoded; +} + } std::string RestCatalog::Config::toString() const @@ -468,7 +486,8 @@ RestCatalog::Namespaces RestCatalog::parseNamespaces(DB::ReadBuffer & buf, const DB::Names RestCatalog::getTables(const std::string & base_namespace, size_t limit) const { - const std::string endpoint = std::filesystem::path(NAMESPACES_ENDPOINT) / base_namespace / "tables"; + auto encoded_namespace = encodeNamespaceForURI(base_namespace); + const std::string endpoint = std::filesystem::path(NAMESPACES_ENDPOINT) / encoded_namespace / "tables"; auto buf = createReadBuffer(config.prefix / endpoint); return parseTables(*buf, base_namespace, limit); @@ -562,7 +581,8 @@ bool RestCatalog::getTableMetadataImpl( headers.emplace_back("X-Iceberg-Access-Delegation", "vended-credentials"); } - const std::string endpoint = std::filesystem::path(NAMESPACES_ENDPOINT) / namespace_name / "tables" / table_name; + auto encoded_namespace = encodeNamespaceForURI(namespace_name); + const std::string endpoint = std::filesystem::path(NAMESPACES_ENDPOINT) / encoded_namespace / "tables" / table_name; auto buf = createReadBuffer(config.prefix / endpoint, /* params */{}, headers); if (buf->eof())