Skip to content

Commit 5437009

Browse files
SteNicholasclaude
andcommitted
feat(rest): support rest catalog with database/table operations and snapshot listing
Add a REST catalog implementation selected via the "metastore=rest" option: - RestHttpClient: blocking libcurl client with exponential-backoff retries (429/503 retried for all methods, transport errors only for idempotent ones, honoring a Retry-After header in both the delta-seconds and the HTTP-date form when it yields a positive delay, with a locale-independent date parse; a server closing the connection without responding is not retried) and per-request debug logging. - RestApi: HTTP + JSON layer with "/v1/config" option merging (overrides > client options > defaults), "header." options as request headers, paged listing and error-to-Status mapping that prefers the code of the parsed error body over the http status, redacts sensitive server messages, never echoes response bodies into error messages (they may carry credentials), carries the request id (x-request-id, falling back to any request-id header) and attaches a RestErrorDetail with the mapped code so callers can distinguish e.g. authentication failures programmatically. - RestCatalog: database/table operations and snapshot listing, "table-default." option defaults, system/branch table checks and schema conversion with the computed highestFieldId, rejecting duplicated field ids at any nesting level. Branch identifiers are passed through to the server so it resolves the branch and returns the branch's own schema (the default branch "main" resolves to the bare table), snapshots of a branch are listed under the branch object name, and the "sys" database serves the local global system tables like FileSystemCatalog. - Bear token authentication provider. - CatalogOptions: catalog-level option keys (metastore, uri, token, token.provider) and the "table-default." option prefix in a dedicated public header, keeping them separate from the table-level CoreOptions. - CatalogUtils: system database and system/branch table checks shared by FileSystemCatalog and RestCatalog; FileSystemCatalog's create/drop/rename checks now use them instead of inline messages. - SpecialFieldIds::SYSTEM_FIELD_ID_START names the reserved system field id bound, which is excluded from the highest field id. The pieces overlapping with the object store file systems are unified under common/utils: UrlUtils carries both URL encoding flavors (form-urlencoded for the REST api, RFC 3986 for S3), the generic HTTP client moved there from common/fs, and the rest client reuses its curl global-init guard, ScopeGuard cleanup and header-line parsing. libcurl detection in CMake is a single find_package guarded by PAIMON_ENABLE_S3 OR PAIMON_ENABLE_REST. libcurl is used from the system rather than bundled, so the new PAIMON_ENABLE_REST option defaults to OFF like the other optional components with external requirements and is documented alongside them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7a5e4f8 commit 5437009

45 files changed

Lines changed: 5407 additions & 135 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CMakeLists.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,17 @@ option(PAIMON_ENABLE_LUMINA "Whether to enable lumina vector index" OFF)
6666
option(PAIMON_ENABLE_LUCENE "Whether to enable lucene index" OFF)
6767
option(PAIMON_ENABLE_TANTIVY
6868
"Whether to enable tantivy-fulltext global index (Rust FFI, experimental)" OFF)
69+
option(PAIMON_ENABLE_REST "Whether to enable the rest catalog (requires libcurl)" OFF)
6970
if(PAIMON_ENABLE_ORC)
7071
add_definitions(-DPAIMON_ENABLE_ORC)
7172
endif()
73+
if(PAIMON_ENABLE_REST)
74+
add_definitions(-DPAIMON_ENABLE_REST)
75+
endif()
76+
# libcurl backs the HTTP client shared by the S3 file system and the rest catalog.
77+
if(PAIMON_ENABLE_S3 OR PAIMON_ENABLE_REST)
78+
find_package(CURL REQUIRED)
79+
endif()
7280
if(PAIMON_ENABLE_AVRO)
7381
add_definitions(-DPAIMON_ENABLE_AVRO)
7482
endif()
@@ -487,9 +495,6 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/PaimonConfig.cmake"
487495
488496
config_summary_message()
489497
490-
if(PAIMON_ENABLE_S3)
491-
find_package(CURL REQUIRED)
492-
endif()
493498
add_subdirectory(src/paimon)
494499
add_subdirectory(src/paimon/fs/local)
495500
add_subdirectory(src/paimon/fs/jindo)

ci/scripts/build_paimon.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ CMAKE_ARGS=(
130130
"-DPAIMON_ENABLE_LUMINA=${ENABLE_LUMINA}"
131131
"-DPAIMON_ENABLE_LUCENE=ON"
132132
"-DPAIMON_ENABLE_TANTIVY=${ENABLE_TANTIVY}"
133+
"-DPAIMON_ENABLE_REST=ON"
133134
"-DPAIMON_LINT_GIT_TARGET_COMMIT=${lint_git_target_commit}"
134135
)
135136

docs/source/building.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ boolean flags to ``cmake``.
145145
* ``-DPAIMON_ENABLE_AVRO=ON``: Apache Avro libraries and Paimon integration
146146
* ``-DPAIMON_ENABLE_JINDO=ON``: Support for Alibaba Jindo filesystems
147147
* ``-DPAIMON_ENABLE_LUMINA=ON``: Support for Lumina vector index, lumina is only supported on gcc9 or higher.
148+
* ``-DPAIMON_ENABLE_REST=ON``: Support for the REST catalog (``metastore=rest``), requires the libcurl development package.
148149

149150
Third-party dependency source
150151
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

include/paimon/catalog_options.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include "paimon/visibility.h"
20+
21+
namespace paimon {
22+
23+
/// Catalog-level configuration option keys; table-level keys live in `Options`.
24+
struct PAIMON_EXPORT CatalogOptions {
25+
/// "metastore" - Metastore of the paimon catalog.
26+
/// Supported values are "filesystem" (default) and "rest".
27+
static const char METASTORE[];
28+
29+
/// "uri" - Server url of the REST catalog. Only used when METASTORE is "rest".
30+
static const char URI[];
31+
32+
/// "token" - Token of the "bear" token provider of the REST catalog.
33+
static const char TOKEN[];
34+
35+
/// "token.provider" - Authentication provider of the REST catalog.
36+
/// Currently only "bear" is supported ("bear" is the protocol's historical
37+
/// spelling of "bearer", do not "fix" it).
38+
static const char TOKEN_PROVIDER[];
39+
40+
/// "table-default." - Prefix of the catalog options that provide table option
41+
/// defaults: "table-default.<key>=<value>" applies "<key>=<value>" to a created
42+
/// table when the caller left "<key>" unset.
43+
static const char TABLE_DEFAULT_OPTION_PREFIX[];
44+
};
45+
46+
} // namespace paimon

include/paimon/utils/special_field_ids.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class SpecialFieldIds {
3939

4040
/// Special field ID reserved for index score. Value: CPP_FIELD_ID_END - 1
4141
static const int32_t INDEX_SCORE = CPP_FIELD_ID_END - 1;
42+
43+
/// The lowest field ID reserved for system fields; IDs at or above this bound are
44+
/// excluded when computing the highest field ID of a schema. Value: INT32_MAX / 2
45+
static const int32_t SYSTEM_FIELD_ID_START = std::numeric_limits<int32_t>::max() / 2;
4246
};
4347

4448
} // namespace paimon

src/paimon/CMakeLists.txt

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
set(PAIMON_COMMON_SRCS
16+
common/catalog_options.cpp
1617
common/compression/block_compression_factory.cpp
1718
common/compression/block_compressor.cpp
1819
common/compression/block_decompressor.cpp
@@ -176,12 +177,18 @@ set(PAIMON_COMMON_SRCS
176177
common/utils/roaring_bitmap64.cpp
177178
common/utils/row_range_index.cpp
178179
common/utils/status.cpp
179-
common/utils/string_utils.cpp)
180+
common/utils/string_utils.cpp
181+
common/utils/url_utils.cpp)
180182

183+
# The shared HTTP client is used by both the object store file systems and the
184+
# rest catalog.
185+
set(PAIMON_CURL_LINK_LIBS)
186+
if(PAIMON_ENABLE_S3 OR PAIMON_ENABLE_REST)
187+
list(APPEND PAIMON_COMMON_SRCS common/utils/http_client.cpp)
188+
set(PAIMON_CURL_LINK_LIBS CURL::libcurl)
189+
endif()
181190
if(PAIMON_ENABLE_S3)
182-
list(APPEND PAIMON_COMMON_SRCS common/fs/http_client.cpp
183-
common/fs/object_store_file_system.cpp)
184-
set(PAIMON_OBJECT_STORE_LINK_LIBS CURL::libcurl)
191+
list(APPEND PAIMON_COMMON_SRCS common/fs/object_store_file_system.cpp)
185192
endif()
186193

187194
set(PAIMON_CORE_SRCS
@@ -224,6 +231,7 @@ set(PAIMON_CORE_SRCS
224231
core/casting/timestamp_to_timestamp_cast_executor.cpp
225232
core/casting/casting_utils.cpp
226233
core/catalog/catalog.cpp
234+
core/catalog/catalog_utils.cpp
227235
core/catalog/file_system_catalog.cpp
228236
core/catalog/identifier.cpp
229237
core/core_options.cpp
@@ -403,6 +411,18 @@ set(PAIMON_CORE_SRCS
403411
core/utils/special_field_ids.cpp
404412
core/utils/tag_manager.cpp)
405413

414+
if(PAIMON_ENABLE_REST)
415+
list(APPEND
416+
PAIMON_CORE_SRCS
417+
rest/rest_http_client.cpp
418+
rest/resource_paths.cpp
419+
rest/rest_api.cpp
420+
rest/rest_auth.cpp
421+
rest/rest_catalog.cpp
422+
rest/rest_messages.cpp
423+
rest/rest_util.cpp)
424+
endif()
425+
406426
add_paimon_lib(paimon
407427
SOURCES
408428
${PAIMON_COMMON_SRCS}
@@ -416,7 +436,7 @@ add_paimon_lib(paimon
416436
xxhash
417437
Threads::Threads
418438
RapidJSON
419-
${PAIMON_OBJECT_STORE_LINK_LIBS}
439+
${PAIMON_CURL_LINK_LIBS}
420440
STATIC_LINK_LIBS
421441
arrow
422442
tbb
@@ -426,14 +446,21 @@ add_paimon_lib(paimon
426446
xxhash
427447
Threads::Threads
428448
RapidJSON
429-
${PAIMON_OBJECT_STORE_LINK_LIBS}
449+
${PAIMON_CURL_LINK_LIBS}
430450
SHARED_LINK_FLAGS
431451
${PAIMON_VERSION_SCRIPT_FLAGS})
432452

433453
add_subdirectory(common/file_index)
434454
add_subdirectory(common/global_index)
435455

436456
if(PAIMON_BUILD_TESTS)
457+
# The shared HTTP client is compiled only for the components that need libcurl,
458+
# so its test follows the same gate.
459+
set(PAIMON_COMMON_HTTP_CLIENT_TEST_SRCS)
460+
if(PAIMON_ENABLE_S3 OR PAIMON_ENABLE_REST)
461+
set(PAIMON_COMMON_HTTP_CLIENT_TEST_SRCS common/utils/http_client_test.cpp)
462+
endif()
463+
437464
add_paimon_test(memory_test
438465
SOURCES
439466
common/memory/memory_pool_test.cpp
@@ -594,6 +621,8 @@ if(PAIMON_BUILD_TESTS)
594621
common/utils/status_test.cpp
595622
common/utils/stream_utils_test.cpp
596623
common/utils/string_utils_test.cpp
624+
common/utils/url_utils_test.cpp
625+
${PAIMON_COMMON_HTTP_CLIENT_TEST_SRCS}
597626
common/utils/range_test.cpp
598627
common/utils/uuid_test.cpp
599628
common/utils/decimal_utils_test.cpp
@@ -870,4 +899,18 @@ if(PAIMON_BUILD_TESTS)
870899
EXTRA_INCLUDES
871900
${JINDOSDK_INCLUDE_DIR})
872901

902+
if(PAIMON_ENABLE_REST)
903+
add_paimon_test(rest_test
904+
SOURCES
905+
rest/rest_http_client_test.cpp
906+
rest/mock_rest_server.cpp
907+
rest/rest_catalog_test.cpp
908+
rest/rest_messages_test.cpp
909+
STATIC_LINK_LIBS
910+
paimon_shared
911+
test_utils_static
912+
${TEST_STATIC_LINK_LIBS}
913+
${GTEST_LINK_TOOLCHAIN})
914+
endif()
915+
873916
endif()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "paimon/catalog_options.h"
18+
19+
namespace paimon {
20+
21+
const char CatalogOptions::METASTORE[] = "metastore";
22+
const char CatalogOptions::URI[] = "uri";
23+
const char CatalogOptions::TOKEN[] = "token";
24+
const char CatalogOptions::TOKEN_PROVIDER[] = "token.provider";
25+
const char CatalogOptions::TABLE_DEFAULT_OPTION_PREFIX[] = "table-default.";
26+
27+
} // namespace paimon
Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
#include "paimon/common/fs/http_client.h"
17+
#include "paimon/common/utils/http_client.h"
1818

1919
#include <curl/curl.h>
2020

@@ -44,11 +44,6 @@ class CurlGlobalGuard {
4444
}
4545
};
4646

47-
std::shared_ptr<CurlGlobalGuard> GetCurlGlobalGuard() {
48-
static auto guard = std::make_shared<CurlGlobalGuard>();
49-
return guard;
50-
}
51-
5247
void TrimHttpWhitespace(std::string* value) {
5348
constexpr char kHttpWhitespace[] = " \t\r\n";
5449
size_t begin = value->find_first_not_of(kHttpWhitespace);
@@ -92,16 +87,7 @@ size_t WriteCallback(char* data, size_t size, size_t count, void* user_data) {
9287
size_t HeaderCallback(char* data, size_t size, size_t count, void* user_data) {
9388
auto* context = static_cast<TransferContext*>(user_data);
9489
size_t bytes = size * count;
95-
std::string line(data, bytes);
96-
size_t colon = line.find(':');
97-
if (colon != std::string::npos) {
98-
std::string name = line.substr(0, colon);
99-
TrimHttpWhitespace(&name);
100-
name = StringUtils::ToLowerCase(name);
101-
std::string value = line.substr(colon + 1);
102-
TrimHttpWhitespace(&value);
103-
context->response.headers[name] = std::move(value);
104-
}
90+
ParseHttpHeaderLine(data, bytes, &context->response.headers);
10591
return bytes;
10692
}
10793

@@ -116,9 +102,31 @@ bool IsRetryable(CURLcode code, int64_t status_code) {
116102

117103
} // namespace
118104

105+
std::shared_ptr<void> EnsureCurlGlobalInit() {
106+
static auto guard = std::make_shared<CurlGlobalGuard>();
107+
return guard;
108+
}
109+
110+
void ParseHttpHeaderLine(const char* data, size_t size, HttpHeaders* headers) {
111+
std::string line(data, size);
112+
size_t colon = line.find(':');
113+
if (colon == std::string::npos) {
114+
return;
115+
}
116+
std::string name = line.substr(0, colon);
117+
TrimHttpWhitespace(&name);
118+
if (name.empty()) {
119+
return;
120+
}
121+
name = StringUtils::ToLowerCase(name);
122+
std::string value = line.substr(colon + 1);
123+
TrimHttpWhitespace(&value);
124+
(*headers)[name] = std::move(value);
125+
}
126+
119127
class CurlHttpClient::Impl {
120128
public:
121-
Impl() : guard_(GetCurlGlobalGuard()) {}
129+
Impl() : guard_(EnsureCurlGlobalInit()) {}
122130

123131
~Impl() {
124132
for (CURL* handle : handles_) {
@@ -143,7 +151,7 @@ class CurlHttpClient::Impl {
143151
}
144152

145153
private:
146-
std::shared_ptr<CurlGlobalGuard> guard_;
154+
std::shared_ptr<void> guard_;
147155
mutable std::mutex mutex_;
148156
mutable std::vector<CURL*> handles_;
149157
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ enum class HttpMethod { HEAD, GET };
3131
using HttpHeaders = std::map<std::string, std::string>;
3232
using HttpBodyConsumer = std::function<Status(const char*, int64_t)>;
3333

34+
/// Ensures libcurl's global state is initialized; the returned guard keeps it alive.
35+
PAIMON_EXPORT std::shared_ptr<void> EnsureCurlGlobalInit();
36+
37+
/// Parses one raw HTTP header line into `headers`, lower-casing the name and trimming
38+
/// HTTP whitespace around the name and value; lines without a ':' or with an empty
39+
/// name are ignored.
40+
PAIMON_EXPORT void ParseHttpHeaderLine(const char* data, size_t size, HttpHeaders* headers);
41+
3442
struct HttpRequest {
3543
HttpMethod method = HttpMethod::GET;
3644
std::string url;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "paimon/common/utils/http_client.h"
18+
19+
#include <string>
20+
21+
#include "gtest/gtest.h"
22+
23+
namespace paimon::test {
24+
25+
TEST(HttpClientUtilTest, ParseHttpHeaderLine) {
26+
HttpHeaders headers;
27+
const std::string line = "Content-Type: application/json\r\n";
28+
ParseHttpHeaderLine(line.data(), line.size(), &headers);
29+
ASSERT_EQ((HttpHeaders{{"content-type", "application/json"}}), headers);
30+
31+
const std::string overwrite = "content-type: text/PLAIN \r\n";
32+
ParseHttpHeaderLine(overwrite.data(), overwrite.size(), &headers);
33+
ASSERT_EQ((HttpHeaders{{"content-type", "text/PLAIN"}}), headers);
34+
35+
// Lines without a colon (like the status line) and empty names are ignored.
36+
const std::string status_line = "HTTP/1.1 200 OK\r\n";
37+
ParseHttpHeaderLine(status_line.data(), status_line.size(), &headers);
38+
const std::string empty_name = ": value\r\n";
39+
ParseHttpHeaderLine(empty_name.data(), empty_name.size(), &headers);
40+
ASSERT_EQ(1, headers.size());
41+
42+
const std::string empty_value = "x-empty:\r\n";
43+
ParseHttpHeaderLine(empty_value.data(), empty_value.size(), &headers);
44+
ASSERT_EQ("", headers.at("x-empty"));
45+
}
46+
47+
} // namespace paimon::test

0 commit comments

Comments
 (0)