Skip to content

feat(rest): support rest catalog for databases, tables and snapshots - #165

Merged
SteNicholas merged 1 commit into
apache:mainfrom
SteNicholas:PAIMON-164
Aug 3, 2026
Merged

feat(rest): support rest catalog for databases, tables and snapshots#165
SteNicholas merged 1 commit into
apache:mainfrom
SteNicholas:PAIMON-164

Conversation

@SteNicholas

Copy link
Copy Markdown
Member

Purpose

Linked issue: #164

Add a REST catalog implementation, selected via the metastore=rest option, that talks the Paimon REST catalog open API.

New components

  • RestHttpClient — blocking libcurl client with exponential-backoff retries:
    • 429/503 responses are retried for all methods, transport errors only for idempotent methods via an explicit allowlist of transient failures (name-resolution and connect failures, timeouts, TLS failures and a server closing the connection without responding are never retried).
    • A Retry-After response header — delta-seconds or HTTP-date form, parsed locale-independently — takes precedence over the backoff when it yields a positive delay. Every retry sleep is bounded by a per-sleep cap and an overall retry budget; a Retry-After beyond either bound stops retrying rather than sleeping less than the server requested.
    • Redirects keep the method and body of POST/DELETE requests (CURLOPT_POSTREDIR) and are restricted to http(s) targets.
    • Header names must be valid HTTP tokens and header values must not contain CR/LF/NUL; a violating header fails the request before anything is sent.
    • Easy handles are pooled and reset between requests, so a request reuses the connection opened by the previous one instead of paying a TCP and TLS handshake every time; TLS below 1.2 is refused and requests carry a default paimon-cpp user agent that a header.User-Agent option overrides.
    • Per-request logging carries the request id (debug on success, warn on a retry or a final failure).
  • RestApi — HTTP + JSON layer:
    • /v1/config option merging (overrides > client options > defaults), header. options sent as request headers, and paged listing.
    • Error mapping to Status (404 → NotExist, 409 → Exist, 400 → Invalid, 501 → NotImplemented) that prefers the code of the parsed error body over the http status.
    • Sensitive server messages are redacted, and response bodies are never echoed into error messages (they may carry credentials), so a body that is not an error object is reported as unparsable, distinct from an error object carrying no message.
    • Errors carry the request id (x-request-id, falling back to any request-id header) and attach a RestErrorDetail with the mapped code, so callers can distinguish e.g. authentication failures programmatically.
  • RestCatalog — database/table create/drop/rename/list/get, snapshot listing, table-default. option defaults on table creation, and system/branch table checks:
    • The server table response is converted into a TableSchema with the computed highestFieldId: ids at or above SpecialFieldIds::SYSTEM_FIELD_ID_START are excluded, a duplicated field id at any nesting level is rejected, and a missing or wrong-typed partitionKeys/primaryKeys/options member fails instead of silently defaulting.
    • Branch identifiers are passed through to the server, which resolves the branch and returns the branch's own schema (the default branch main maps case-insensitively to the bare table); snapshots of a branch are listed under the branch object name.
    • The sys database serves the local global system tables like FileSystemCatalog.
  • Bear token authentication provider (token.provider=bear, the protocol's historical spelling of "bearer").

Shared code

  • The pieces overlapping with the object-store file systems are unified under common/utils: the generic HTTP client moved there from common/fs and is shared by the S3 file system and the REST catalog, UrlUtils carries both URL-encoding flavors (form-urlencoded for the REST api, RFC 3986 for S3) with the S3 client reusing it, and libcurl detection in CMake is a single find_package(CURL) gated on PAIMON_ENABLE_S3 OR PAIMON_ENABLE_REST.
  • CatalogUtils holds the system-database and system/branch table checks shared by FileSystemCatalog and RestCatalog, with the check order and messages aligned with the Java CatalogUtils (Cannot 'createTable' for system table ...). FileSystemCatalog now also rejects branch identifiers on create/drop/rename — a branch identifier resolves to the main table directory, so dropping t$branch_b would otherwise delete the whole table.
  • The new SensitiveConfigUtils centralizes credential redaction with the Java SensitiveConfigUtils key markers and masking rule (matched on the key lower-cased with separators removed; a key naming a true secret is masked as a whole, while an identifier-like key such as dlf.access-key-id keeps at most a four-character tail of a long enough value). sys.catalog_options masks every row through it so credentials such as the bearer token never surface to users who can query that table, and a server error message carrying a secret marker is redacted as a whole.
  • RestUtil::ExtractRequestId and RestAuthParameter::Create likewise hold the request-id lookup and the query-parameter encoding in one place, so the log line and the error message report the same id and no call site can sign an unencoded parameter.

Build

libcurl is used from the system rather than bundled, so the new CMake option PAIMON_ENABLE_REST defaults to OFF, consistent with the other optional components that need something outside the bundled third-party toolchain (jindo, lance, lumina, lucene, tantivy). CI enables it explicitly in ci/scripts/build_paimon.sh and installs the libcurl development package, so the REST code and its tests are still built and run on every job. With the option off, Catalog::Create reports that metastore=rest requires a build with PAIMON_ENABLE_REST=ON.

Follow-ups (not in this PR): DLF signing, data-token file IO, paged listing parameters (maxResults/name patterns), and the remaining endpoints (alter/commit/views/partitions/tags/functions).

Tests

New rest_test binary (62 cases, registered under the unittest label so it runs in CI), all against an in-process mock HTTP/REST server:

  • RestHttpClientTest: uri normalization, query encoding, a pooled handle carrying no method or body over from the previous request, retry classification (429/503 retried, 404 and non-allowlisted transport errors such as a refused connection or a redirect loop not, truncated responses retried only for idempotent methods), Retry-After precedence in both forms plus the per-sleep and overall budget bounds, retry exhaustion, redirect following with POST keeping its method and body, rejection of invalid header names/values, and transport errors omitting the url and query.
  • RestUtilTest / ResourcePathsTest / RestMessagesTest, each next to its implementation: prefix extraction and the request-id lookup including the gateway-header fallback, resource path building with url-encoded segments, JSON round trips of all request/response messages, and config merge with null-value filtering (a key present in defaults, client options and overrides at once, plus the null-override and null-default paths).
  • RestCatalogTest: client-side option validation (missing uri/token, unsupported token provider), end-to-end catalog operations including pagination, ignore_if_exists / ignore_if_not_exists paths, client/server headers and the json content type, table-default. defaults, system tables, branch tables resolved by the server, broken-schema rejection (missing and wrong-typed partitionKeys/primaryKeys/options members), a non-empty partition-key round trip, server errors surfacing as errors instead of "does not exist", and snapshot listing with sorting.
  • RestApiErrorTest: http-status-to-Status mapping including the request-id fallback, redaction and the RestErrorDetail code.

The shared pieces are covered next to their implementations:

  • UrlUtilsTest: both encoding flavors, including that a literal % is encoded even when / is preserved, which is what the S3 client relies on.
  • SensitiveConfigUtilsTest: key classification, whole-value and tail-preserving masking, and free-form text redaction.
  • HttpClientUtilTest in the common utils suite.
  • TableSchemaTest.TestComputeHighestFieldId: the highest-field-id computation.
  • SnapshotTest.TestToSnapshotInfo: the snapshot-to-info conversion.
  • FileSystemCatalogTest.TestBranchIdentifierRejectedForTableOperations: the branch rejection.
  • The sys.catalog_options integration test: credential-carrying options (token, access-key secret, account key, credential, SAS) are masked in every emitted row, and an access-key id keeps only its four-character tail.

API and Format

  • New public header include/paimon/catalog_options.h with a PAIMON_EXPORTed CatalogOptions struct holding the catalog-level option keys METASTORE, URI, TOKEN, TOKEN_PROVIDER and the TABLE_DEFAULT_OPTION_PREFIX (table-default.) key prefix; table-level keys stay in Options (defs.h), mirroring the CatalogOptions/CoreOptions separation of the Java implementation.
  • RestCatalog implements the Catalog interface including GetOptions(), which returns the client options merged with the server-side /v1/config.
  • Catalog::Create now dispatches on the metastore option (filesystem remains the default; unknown values are rejected). For metastore=rest, root_path is not a filesystem path but the warehouse (instance) name registered on the REST server, documented on the API.
  • No storage format changes. The wire protocol is the Paimon REST catalog open API.

Documentation

  • docs/source/user_guide/catalog.rst now documents both metastores: the existing filesystem metastore section and a new REST Catalog section covering the PAIMON_ENABLE_REST build requirement, the root_path semantics (warehouse/instance name), the CatalogOptions keys (metastore, uri, token.provider, token, table-default.<key>) and a configuration example, replacing the note that claimed REST catalog support is future work. It also states which parts of the REST catalog are not implemented yet (altering a database or a table, views, functions, partitions, tags, branch management, consumers and the dlf token provider), so the guide is not read as promising them.
  • docs/source/building.rst lists -DPAIMON_ENABLE_REST=ON under the optional components, including its libcurl requirement.

Generative AI tooling

Generated-by: Claude Code (claude-fable-5, claude-opus-5)

🤖 Generated with Claude Code

@SteNicholas
SteNicholas force-pushed the PAIMON-164 branch 2 times, most recently from 54137c1 to b86bf27 Compare August 3, 2026 05:31
@SteNicholas

Copy link
Copy Markdown
Member Author

Cherry pick alibaba/paimon-cpp#441.

Add a REST catalog selected via the "metastore=rest" option:

- RestHttpClient: blocking libcurl client with exponential-backoff
  retries. 429/503 are retried for every method, a transient transport
  error only for an idempotent one; a server closing the connection
  without responding is not retried. A Retry-After header is honored in
  both the delta-seconds and the HTTP-date form with a
  locale-independent parse, and is never shortened: the overall retry
  budget bounds it while the per-sleep cap bounds only the backoff. Easy
  handles are pooled, so a request reuses the previous connection
  instead of paying a handshake every time; TLS below 1.2 is refused and
  requests carry a default user agent. Every transported request is
  debug-logged with the server request id.
- RestApi: HTTP + JSON protocol layer. "/v1/config" merges the server
  options into the client ones (overrides > client options > defaults,
  an override sent as null unsets the client option), "header." options
  are sent as request headers and a listing follows the page tokens to
  the last page. A failed request maps to a Status by the code of the
  parsed error body, falling back to the http status, and carries a
  RestErrorDetail with that code so callers can tell e.g. an
  authentication failure from another IO error. A response body is never
  echoed into an error, since it may carry credentials, so a body that
  is not an error object is only reported as unparsable.
- RestCatalog: database, table and snapshot operations, with
  "table-default." option defaults and a schema conversion computing the
  highest field id (ids at or above the new
  SpecialFieldIds::SYSTEM_FIELD_ID_START are excluded, duplicated ids
  are rejected at any nesting level). A branch stays in the object name
  so the server resolves it and returns the branch's own schema, the
  default branch "main" being matched ignoring case and addressed as the
  bare table. The "sys" database serves the local global system tables
  like FileSystemCatalog.
- Bear token authentication provider ("bear" is the protocol's
  historical spelling of "bearer"). RestAuthParameter::Create
  url-encodes the query parameter values a signing provider signs.
- CatalogOptions: public catalog-level option keys (metastore, uri,
  token, token.provider, "table-default." prefix), kept separate from
  the table-level CoreOptions.
- CatalogUtils: system database, system table and branch checks shared
  by FileSystemCatalog and RestCatalog.
- RestUtil::ExtractRequestId: the request id lookup shared by the debug
  log line and the error message, falling back to any header whose name
  carries a request id since a gateway may report it under its own name.

The pieces overlapping with the object store file systems move to
common/utils: UrlUtils carries both URL encoding flavors
(form-urlencoded for the REST api, RFC 3986 for S3), the generic HTTP
client moves there from common/fs, and the new SensitiveConfigUtils
redacts credentials for every surface that may expose them - the
sys.catalog_options table masks the value of a credential-carrying
option key, keeping at most a short tail of an identifier-like value,
and a server error message carrying a secret marker is redacted as a
whole.

libcurl is used from the system, so the new PAIMON_ENABLE_REST option
defaults to OFF like the other optional components with external
requirements; its CMake detection is a single find_package guarded by
PAIMON_ENABLE_S3 OR PAIMON_ENABLE_REST.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@zjw1111 zjw1111 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@SteNicholas
SteNicholas merged commit 208f65d into apache:main Aug 3, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants