Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions crates/catalog/rest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ chrono = { workspace = true }
http = { workspace = true }
iceberg = { workspace = true }
itertools = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tracing = { workspace = true }
typed-builder = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
uuid = { workspace = true, features = ["v4", "v7"] }

[dev-dependencies]
bytes = { workspace = true }
Expand Down
201 changes: 201 additions & 0 deletions crates/catalog/rest/public-api.txt

Large diffs are not rendered by default.

60 changes: 47 additions & 13 deletions crates/catalog/rest/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ impl RestCatalogConfig {
self.url_prefixed(&["namespaces", &ns.to_url_string(), "register"])
}

fn table_endpoint(&self, table: &TableIdent) -> String {
pub(crate) fn table_endpoint(&self, table: &TableIdent) -> String {
self.url_prefixed(&[
"namespaces",
&table.namespace.to_url_string(),
Expand Down Expand Up @@ -459,14 +459,14 @@ pub(crate) fn oauth_params_from_props(props: &HashMap<String, String>) -> HashMa
}

#[derive(Debug)]
struct RestClient {
pub(crate) struct RestClient {
/// Carries the session the auth manager derived from the merged
/// configuration, so every request below is authenticated.
http_client: HttpClient,
pub(crate) http_client: HttpClient,
/// Runtime config is fetched from rest server and stored here.
///
/// It's could be different from the user config.
config: RestCatalogConfig,
pub(crate) config: RestCatalogConfig,
/// Capabilities the server advertises (see [`RestCatalog::supports_endpoint`]).
endpoints: HashSet<Endpoint>,
}
Expand All @@ -479,7 +479,7 @@ impl RestClient {
}

/// Sends `request`, authenticated by the client's session.
async fn query_catalog(&self, request: HttpRequest) -> Result<Response> {
pub(crate) async fn query_catalog(&self, request: HttpRequest) -> Result<Response> {
self.http_client.query_catalog(request).await
}
}
Expand All @@ -494,7 +494,7 @@ pub struct RestCatalog {
///
/// It could be different from the config fetched from the server and used at runtime.
user_config: RestCatalogConfig,
client: OnceCell<RestClient>,
client: Arc<OnceCell<RestClient>>,
/// Storage factory for creating FileIO instances.
storage_factory: Option<Arc<dyn StorageFactory>>,
runtime: Runtime,
Expand All @@ -504,7 +504,7 @@ pub struct RestCatalog {

impl RestCatalog {
/// Creates a `RestCatalog` from a [`RestCatalogConfig`].
fn new(
pub(crate) fn new(
config: RestCatalogConfig,
auth_manager: Option<Arc<dyn AuthManager>>,
storage_factory: Option<Arc<dyn StorageFactory>>,
Expand All @@ -514,13 +514,43 @@ impl RestCatalog {
Self {
auth_manager,
user_config: config,
client: OnceCell::new(),
client: Arc::new(OnceCell::new()),
storage_factory,
runtime,
kms_client,
}
}

/// Same catalog identity with an empty HTTP client cache, for fire-and-forget
/// work (best-effort plan cancel on drop) that must not borrow `self`.
pub(crate) fn clone_uninitialized(&self) -> Self {
Self::new(
self.user_config.clone(),
self.auth_manager.clone(),
self.storage_factory.clone(),
self.runtime.clone(),
self.kms_client.clone(),
)
}

/// Shares the already-initialized HTTP client. Used when injecting this
/// catalog as a [`iceberg::scan::ScanPlanner`] so Auto/capability checks
/// do not repeat `GET /v1/config`.
pub(crate) fn clone_initialized(&self) -> Self {
Self {
auth_manager: self.auth_manager.clone(),
user_config: self.user_config.clone(),
client: Arc::clone(&self.client),
storage_factory: self.storage_factory.clone(),
runtime: self.runtime.clone(),
kms_client: self.kms_client.clone(),
}
}

pub(crate) fn runtime(&self) -> &Runtime {
&self.runtime
}

/// Sends a DELETE request for the given table, optionally requesting purge.
async fn delete_table(&self, table: &TableIdent, purge: bool) -> Result<()> {
let client = self.client().await?;
Expand Down Expand Up @@ -623,7 +653,7 @@ impl RestCatalog {
}

/// Gets the [`RestClient`] from the catalog.
async fn client(&self) -> Result<&RestClient> {
pub(crate) async fn client(&self) -> Result<&RestClient> {
self.client
.get_or_try_init(|| async {
let http_client = HttpClient::new(&self.user_config)?;
Expand Down Expand Up @@ -1066,7 +1096,8 @@ impl Catalog for RestCatalog {
.identifier(table_ident.clone())
.file_io(file_io)
.metadata(response.metadata)
.runtime(self.runtime.clone());
.runtime(self.runtime.clone())
.scan_planner(Arc::new(self.clone_initialized()));
if let Some(kms_client) = self.kms_client.clone() {
table_builder = table_builder.kms_client(kms_client);
}
Expand Down Expand Up @@ -1127,7 +1158,8 @@ impl Catalog for RestCatalog {
.identifier(table_ident.clone())
.file_io(file_io)
.metadata(response.metadata)
.runtime(self.runtime.clone());
.runtime(self.runtime.clone())
.scan_planner(Arc::new(self.clone_initialized()));
if let Some(kms_client) = self.kms_client.clone() {
table_builder = table_builder.kms_client(kms_client);
}
Expand Down Expand Up @@ -1264,7 +1296,8 @@ impl Catalog for RestCatalog {
.file_io(file_io)
.metadata(response.metadata)
.metadata_location(metadata_location.clone())
.runtime(self.runtime.clone());
.runtime(self.runtime.clone())
.scan_planner(Arc::new(self.clone_initialized()));
if let Some(kms_client) = self.kms_client.clone() {
table_builder = table_builder.kms_client(kms_client);
}
Expand Down Expand Up @@ -1341,7 +1374,8 @@ impl Catalog for RestCatalog {
.file_io(file_io)
.metadata(response.metadata)
.metadata_location(response.metadata_location)
.runtime(self.runtime.clone());
.runtime(self.runtime.clone())
.scan_planner(Arc::new(self.clone_initialized()));
if let Some(kms_client) = self.kms_client.clone() {
table_builder = table_builder.kms_client(kms_client);
}
Expand Down
28 changes: 28 additions & 0 deletions crates/catalog/rest/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ endpoints! {
V1_REGISTER_TABLE => POST "/v1/{prefix}/namespaces/{namespace}/register",
V1_REPORT_METRICS => POST "/v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics",
V1_COMMIT_TRANSACTION => POST "/v1/{prefix}/transactions/commit",
V1_PLAN_TABLE_SCAN => POST "/v1/{prefix}/namespaces/{namespace}/tables/{table}/plan",
V1_FETCH_PLAN_RESULT => GET "/v1/{prefix}/namespaces/{namespace}/tables/{table}/plan/{plan-id}",
V1_CANCEL_PLANNING => DELETE "/v1/{prefix}/namespaces/{namespace}/tables/{table}/plan/{plan-id}",
V1_FETCH_SCAN_TASKS => POST "/v1/{prefix}/namespaces/{namespace}/tables/{table}/tasks",
}

/// The standard v1 endpoints assumed to be supported when a server's
Expand Down Expand Up @@ -232,4 +236,28 @@ mod tests {
fn normalizes_http_method_to_uppercase() {
assert_eq!("get /v1/x".parse::<Endpoint>().unwrap().method(), "GET");
}

#[test]
fn scan_planning_endpoints_are_optional_and_not_in_the_default_set() {
assert_eq!(
V1_PLAN_TABLE_SCAN.to_string(),
"POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan"
);
assert_eq!(
V1_FETCH_PLAN_RESULT.to_string(),
"GET /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan/{plan-id}"
);
assert_eq!(
V1_CANCEL_PLANNING.to_string(),
"DELETE /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan/{plan-id}"
);
assert_eq!(
V1_FETCH_SCAN_TASKS.to_string(),
"POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/tasks"
);
assert!(!DEFAULT_ENDPOINTS.contains(&V1_PLAN_TABLE_SCAN));
assert!(!DEFAULT_ENDPOINTS.contains(&V1_FETCH_PLAN_RESULT));
assert!(!DEFAULT_ENDPOINTS.contains(&V1_CANCEL_PLANNING));
assert!(!DEFAULT_ENDPOINTS.contains(&V1_FETCH_SCAN_TASKS));
}
}
3 changes: 3 additions & 0 deletions crates/catalog/rest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ pub use client::HttpClient;
mod request;
pub use request::{HttpRequest, HttpRequestBody};
mod endpoint;
mod scan_decode;
mod scan_planning;
mod types;

pub use auth::*;
pub use catalog::*;
pub use endpoint::Endpoint;
pub use scan_planning::*;
pub use types::*;
Loading
Loading