From 8b2a1f25d9db25f264ea0a975de6d5e86b28e2ac Mon Sep 17 00:00:00 2001 From: Karun Anantharaman <105210556+karunmotorq@users.noreply.github.com> Date: Tue, 6 Jan 2026 06:27:13 +0000 Subject: [PATCH] Fix nested namespace encoding in REST catalog Add encodeNamespaceForURI function to properly encode multi-level namespaces using the unit separator (%1F) as required by the Iceberg REST Catalog API specification. This fixes the issue where tables in nested namespaces (e.g., teslasource.kafkav6.source-data) were not discoverable because ClickHouse was using dot separators in the URL path instead of %1F. Fixes: Tables in hierarchical namespaces return 404 errors Reference: https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml --- src/Databases/Iceberg/RestCatalog.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) 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())