Skip to content

Commit 57ec6b8

Browse files
authored
feat(rest): add scan plan endpoint support to REST catalog client (#783)
When a table is loaded from a REST catalog with scan-planning-mode=server (table config takes precedence over catalog config), NewScan() now returns a RestTableScanBuilder whose Build() produces a RestTableScan. PlanFiles() and PlanFilesStream() on that scan delegate manifest resolution to the server via POST /plan, GET /plan/{id} (with exponential backoff), POST /tasks, and DELETE /plan/{id} (best-effort cancel), instead of reading manifests locally. - Add RestTable, RestTableScanBuilder, RestTableScan and RestScanContext - Promote DataTableScan::PlanFilesStream, TableScan::io and TableScanBuilder::Build to virtual - Convert RestCatalog::client_ and paths_ to shared_ptr so RestScanContext can share ownership with live scans - Add REST incremental append planning for scans with an exclusive start snapshot; scans without one (including an inclusive root snapshot) return NotSupported - Use storage credentials returned by planning responses in a scan-scoped FileIO
1 parent a036fae commit 57ec6b8

29 files changed

Lines changed: 3097 additions & 83 deletions

‎src/iceberg/catalog/rest/CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ set(ICEBERG_REST_SOURCES
3434
rest_catalog.cc
3535
rest_file_io.cc
3636
rest_metrics_reporter.cc
37+
rest_table.cc
38+
rest_table_scan.cc
3739
rest_util.cc
3840
types.cc)
3941

‎src/iceberg/catalog/rest/catalog_properties.cc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include "iceberg/catalog/rest/catalog_properties.h"
2121

2222
#include <algorithm>
23+
#include <cstdint>
24+
#include <optional>
2325
#include <string>
2426
#include <string_view>
2527

@@ -61,4 +63,14 @@ Result<SnapshotMode> RestCatalogProperties::SnapshotLoadingMode() const {
6163
}
6264
}
6365

66+
Result<std::optional<ScanPlanningMode>> RestCatalogProperties::ScanPlanningModeFrom(
67+
const std::unordered_map<std::string, std::string>& config) {
68+
auto it = config.find(kScanPlanningMode.key());
69+
if (it == config.end()) return std::nullopt;
70+
std::string lower = StringUtils::ToLower(it->second);
71+
if (lower == "client") return ScanPlanningMode::kClient;
72+
if (lower == "server") return ScanPlanningMode::kServer;
73+
return InvalidArgument("Invalid scan planning mode: '{}'.", it->second);
74+
}
75+
6476
} // namespace iceberg::rest

‎src/iceberg/catalog/rest/catalog_properties.h‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#pragma once
2121

22+
#include <cstdint>
2223
#include <string>
2324
#include <unordered_map>
2425

@@ -34,6 +35,9 @@ namespace iceberg::rest {
3435
/// \brief Snapshot loading mode for REST catalog.
3536
enum class SnapshotMode : uint8_t { kAll, kRefs };
3637

38+
/// \brief Scan planning mode for REST catalog.
39+
enum class ScanPlanningMode : uint8_t { kClient, kServer };
40+
3741
/// \brief Configuration class for a REST Catalog.
3842
class ICEBERG_REST_EXPORT RestCatalogProperties
3943
: public ConfigBase<RestCatalogProperties> {
@@ -58,6 +62,14 @@ class ICEBERG_REST_EXPORT RestCatalogProperties
5862
/// \brief Whether to report metrics to the REST catalog server (default: true).
5963
inline static Entry<std::string> kMetricsReportingEnabled{
6064
"rest-metrics-reporting-enabled", "true"};
65+
/// \brief The scan planning mode (client or server).
66+
inline static Entry<std::string> kScanPlanningMode{"scan-planning-mode", "client"};
67+
/// \brief Maximum time to wait for a submitted scan plan, in milliseconds.
68+
inline static Entry<int64_t> kScanPlanningPollTimeoutMs{
69+
"rest-scan-planning.poll-timeout-ms", 300000};
70+
/// \brief Extra polls after the first fetch of a submitted scan plan.
71+
inline static Entry<int32_t> kScanPlanningPollNumRetries{
72+
"rest-scan-planning.poll-num-retries", 10};
6173
/// \brief The prefix for HTTP headers.
6274
inline static constexpr std::string_view kHeaderPrefix = "header.";
6375

@@ -80,6 +92,11 @@ class ICEBERG_REST_EXPORT RestCatalogProperties
8092
/// "REFS", or an error if the value is invalid. Parsing is
8193
/// case-insensitive to match Java behavior.
8294
Result<SnapshotMode> SnapshotLoadingMode() const;
95+
96+
/// \brief Get the scan planning mode from the given config map, returning
97+
/// std::nullopt if the key is absent.
98+
static Result<std::optional<ScanPlanningMode>> ScanPlanningModeFrom(
99+
const std::unordered_map<std::string, std::string>& config);
83100
};
84101

85102
} // namespace iceberg::rest

‎src/iceberg/catalog/rest/http_client.cc‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ class HttpResponse::Impl {
5050
};
5151

5252
HttpResponse::HttpResponse() = default;
53+
54+
HttpResponse::HttpResponse(int32_t status_code, std::string body) {
55+
cpr::Response cpr_response;
56+
cpr_response.status_code = status_code;
57+
cpr_response.text = std::move(body);
58+
impl_ = std::make_unique<HttpResponse::Impl>(std::move(cpr_response));
59+
}
60+
5361
HttpResponse::~HttpResponse() = default;
5462
HttpResponse::HttpResponse(HttpResponse&&) noexcept = default;
5563
HttpResponse& HttpResponse::operator=(HttpResponse&&) noexcept = default;

‎src/iceberg/catalog/rest/http_client.h‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ namespace iceberg::rest {
4545
class ICEBERG_REST_EXPORT HttpResponse {
4646
public:
4747
HttpResponse();
48+
HttpResponse(int32_t status_code, std::string body);
4849
~HttpResponse();
4950

5051
HttpResponse(const HttpResponse&) = delete;
@@ -71,44 +72,43 @@ class ICEBERG_REST_EXPORT HttpResponse {
7172
class ICEBERG_REST_EXPORT HttpClient {
7273
public:
7374
explicit HttpClient(std::unordered_map<std::string, std::string> default_headers = {});
74-
~HttpClient();
75+
virtual ~HttpClient();
7576

7677
HttpClient(const HttpClient&) = delete;
7778
HttpClient& operator=(const HttpClient&) = delete;
7879
HttpClient(HttpClient&&) = delete;
7980
HttpClient& operator=(HttpClient&&) = delete;
8081

8182
/// \brief Sends a GET request.
82-
Result<HttpResponse> Get(const std::string& path,
83-
const std::unordered_map<std::string, std::string>& params,
84-
const std::unordered_map<std::string, std::string>& headers,
85-
const ErrorHandler& error_handler, auth::AuthSession& session);
83+
virtual Result<HttpResponse> Get(
84+
const std::string& path, const std::unordered_map<std::string, std::string>& params,
85+
const std::unordered_map<std::string, std::string>& headers,
86+
const ErrorHandler& error_handler, auth::AuthSession& session);
8687

8788
/// \brief Sends a POST request.
88-
Result<HttpResponse> Post(const std::string& path, const std::string& body,
89-
const std::unordered_map<std::string, std::string>& headers,
90-
const ErrorHandler& error_handler,
91-
auth::AuthSession& session);
89+
virtual Result<HttpResponse> Post(
90+
const std::string& path, const std::string& body,
91+
const std::unordered_map<std::string, std::string>& headers,
92+
const ErrorHandler& error_handler, auth::AuthSession& session);
9293

9394
/// \brief Sends a POST request with form data.
94-
Result<HttpResponse> PostForm(
95+
virtual Result<HttpResponse> PostForm(
9596
const std::string& path,
9697
const std::unordered_map<std::string, std::string>& form_data,
9798
const std::unordered_map<std::string, std::string>& headers,
9899
const ErrorHandler& error_handler, auth::AuthSession& session);
99100

100101
/// \brief Sends a HEAD request.
101-
Result<HttpResponse> Head(const std::string& path,
102-
const std::unordered_map<std::string, std::string>& headers,
103-
const ErrorHandler& error_handler,
104-
auth::AuthSession& session);
102+
virtual Result<HttpResponse> Head(
103+
const std::string& path,
104+
const std::unordered_map<std::string, std::string>& headers,
105+
const ErrorHandler& error_handler, auth::AuthSession& session);
105106

106107
/// \brief Sends a DELETE request.
107-
Result<HttpResponse> Delete(const std::string& path,
108-
const std::unordered_map<std::string, std::string>& params,
109-
const std::unordered_map<std::string, std::string>& headers,
110-
const ErrorHandler& error_handler,
111-
auth::AuthSession& session);
108+
virtual Result<HttpResponse> Delete(
109+
const std::string& path, const std::unordered_map<std::string, std::string>& params,
110+
const std::unordered_map<std::string, std::string>& headers,
111+
const ErrorHandler& error_handler, auth::AuthSession& session);
112112

113113
private:
114114
std::unordered_map<std::string, std::string> default_headers_;

‎src/iceberg/catalog/rest/json_serde.cc‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ Result<StorageCredential> StorageCredentialFromJson(const nlohmann::json& json)
128128
return credential;
129129
}
130130

131-
/// \brief Reads the optional `storage-credentials` array shared by the
132-
/// LoadTable and LoadCredentials responses.
131+
/// \brief Reads the optional `storage-credentials` array.
133132
Result<std::vector<StorageCredential>> StorageCredentialsFromJson(
134133
const nlohmann::json& json) {
135134
std::vector<StorageCredential> credentials;
@@ -146,6 +145,21 @@ Result<std::vector<StorageCredential>> StorageCredentialsFromJson(
146145
}
147146
return credentials;
148147
}
148+
149+
/// \brief Writes `storage-credentials` to json if any are present.
150+
Status StorageCredentialsToJson(const std::vector<StorageCredential>& credentials,
151+
nlohmann::json& json) {
152+
if (credentials.empty()) {
153+
return {};
154+
}
155+
nlohmann::json creds_json = nlohmann::json::array();
156+
for (const auto& cred : credentials) {
157+
ICEBERG_ASSIGN_OR_RAISE(auto entry, StorageCredentialToJson(cred));
158+
creds_json.push_back(std::move(entry));
159+
}
160+
json[kStorageCredentials] = std::move(creds_json);
161+
return {};
162+
}
149163
} // namespace
150164

151165
Result<std::vector<std::shared_ptr<FileScanTask>>> FileScanTasksFromJson(
@@ -829,6 +843,8 @@ Result<nlohmann::json> ToJson(const PlanTableScanRequest& request) {
829843
if (request.snapshot_id.has_value()) {
830844
json[kSnapshotId] = request.snapshot_id.value();
831845
}
846+
// FIXME: Preserve explicit empty projections as select: []; omitting select
847+
// means the default projection. Java currently has the same issue.
832848
if (!request.select.empty()) {
833849
json[kSelect] = request.select;
834850
}
@@ -884,6 +900,7 @@ Result<PlanTableScanResponse> PlanTableScanResponseFromJson(
884900
}
885901
ICEBERG_RETURN_UNEXPECTED(
886902
ScanTaskFieldsFromJson(json, response, partition_specs_by_id, schema));
903+
ICEBERG_ASSIGN_OR_RAISE(response.storage_credentials, StorageCredentialsFromJson(json));
887904
ICEBERG_RETURN_UNEXPECTED(response.Validate());
888905
return response;
889906
}
@@ -895,6 +912,7 @@ Result<nlohmann::json> ToJson(
895912
const Schema& schema) {
896913
ICEBERG_ASSIGN_OR_RAISE(nlohmann::json json,
897914
ScanTaskFieldsToJson(response, partition_specs_by_id, schema));
915+
ICEBERG_RETURN_UNEXPECTED(StorageCredentialsToJson(response.storage_credentials, json));
898916
json[kPlanStatus] = ToString(response.plan_status);
899917
if (!response.plan_id.empty()) {
900918
json[kPlanId] = response.plan_id;
@@ -921,6 +939,7 @@ Result<FetchPlanningResultResponse> FetchPlanningResultResponseFromJson(
921939
}
922940
ICEBERG_RETURN_UNEXPECTED(
923941
ScanTaskFieldsFromJson(json, response, partition_specs_by_id, schema));
942+
ICEBERG_ASSIGN_OR_RAISE(response.storage_credentials, StorageCredentialsFromJson(json));
924943
ICEBERG_RETURN_UNEXPECTED(response.Validate());
925944
return response;
926945
}
@@ -932,6 +951,7 @@ Result<nlohmann::json> ToJson(
932951
const Schema& schema) {
933952
ICEBERG_ASSIGN_OR_RAISE(nlohmann::json json,
934953
ScanTaskFieldsToJson(response, partition_specs_by_id, schema));
954+
ICEBERG_RETURN_UNEXPECTED(StorageCredentialsToJson(response.storage_credentials, json));
935955
json[kPlanStatus] = ToString(response.plan_status);
936956
if (response.error.has_value()) {
937957
json[kError] = ToJson(*response.error)[kError];

0 commit comments

Comments
 (0)