1use std::collections::{HashMap, HashSet};
21use std::fmt::{Debug, Formatter};
22use std::future::Future;
23use std::str::FromStr;
24use std::sync::{Arc, OnceLock};
25
26use async_trait::async_trait;
27use iceberg::encryption::kms::{KeyManagementClient, KmsClientFactory};
28use iceberg::io::{FileIO, FileIOBuilder, StorageFactory};
29use iceberg::table::Table;
30use iceberg::{
31 Catalog, CatalogBuilder, Error, ErrorKind, Namespace, NamespaceIdent, Result, Runtime,
32 SessionCatalog, SessionContext, TableCommit, TableCreation, TableIdent,
33};
34use itertools::Itertools;
35use reqwest::header::{
36 HeaderMap, HeaderName, HeaderValue, {self},
37};
38use reqwest::{Client, Method, StatusCode, Url};
39use tokio::sync::OnceCell;
40use typed_builder::TypedBuilder;
41
42use crate::auth::{AUTH_TYPE_NONE, AUTH_TYPE_OAUTH2, AuthManager, NoopAuthManager, OAuth2Manager};
43use crate::client::{
44 HttpClient, deserialize_catalog_response, deserialize_unexpected_catalog_error,
45};
46use crate::endpoint::{Endpoint, V1_NAMESPACE_EXISTS, V1_TABLE_EXISTS};
47use crate::request::HttpRequest;
48use crate::response::HttpResponse;
49use crate::types::{
50 CatalogConfig, CommitTableRequest, CommitTableResponse, CreateNamespaceRequest,
51 CreateTableRequest, ListNamespaceResponse, ListTablesResponse, LoadTableResult,
52 NamespaceResponse, RegisterTableRequest, RenameTableRequest,
53};
54
55pub const REST_CATALOG_PROP_URI: &str = "uri";
57pub const REST_CATALOG_PROP_WAREHOUSE: &str = "warehouse";
59pub const REST_CATALOG_PROP_DISABLE_HEADER_REDACTION: &str = "disable-header-redaction";
62pub const REST_CATALOG_PROP_AUTH_TYPE: &str = "rest.auth.type";
66
67const ICEBERG_REST_SPEC_VERSION: &str = "0.14.1";
68const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
69const PATH_V1: &str = "v1";
70
71#[derive(Debug, Default)]
77pub struct RestCatalogBuilder {
78 session_context: Option<SessionContext>,
79 inner: RestSessionCatalogBuilder,
80}
81
82impl CatalogBuilder for RestCatalogBuilder {
83 type C = RestCatalog;
84
85 fn with_storage_factory(mut self, storage_factory: Arc<dyn StorageFactory>) -> Self {
86 self.inner = self.inner.with_storage_factory(storage_factory);
87 self
88 }
89
90 fn with_kms_client_factory(mut self, kms_client_factory: Arc<dyn KmsClientFactory>) -> Self {
91 self.inner = self.inner.with_kms_client_factory(kms_client_factory);
92 self
93 }
94
95 fn with_runtime(mut self, runtime: Runtime) -> Self {
96 self.inner = self.inner.with_runtime(runtime);
97 self
98 }
99
100 fn load(
101 self,
102 name: impl Into<String>,
103 props: HashMap<String, String>,
104 ) -> impl Future<Output = Result<Self::C>> + Send {
105 let name = name.into();
106 async move {
107 let context = self.session_context.unwrap_or_else(SessionContext::empty);
108 let session_catalog = Arc::new(self.inner.load(name, props).await?);
109
110 Ok(RestCatalog::from_session_catalog(context, session_catalog))
111 }
112 }
113}
114
115impl RestCatalogBuilder {
116 pub fn with_client(mut self, client: Client) -> Self {
118 self.inner = self.inner.with_client(client);
119 self
120 }
121
122 pub fn with_session_context(mut self, context: SessionContext) -> Self {
127 self.session_context = Some(context);
128 self
129 }
130
131 pub fn with_auth_manager(mut self, auth_manager: Arc<dyn AuthManager>) -> Self {
133 self.inner = self.inner.with_auth_manager(auth_manager);
134 self
135 }
136}
137
138#[derive(Clone, TypedBuilder)]
140pub(crate) struct RestCatalogConfig {
141 #[builder(default, setter(strip_option))]
142 name: Option<String>,
143
144 uri: String,
145
146 #[builder(default, setter(strip_option(fallback = warehouse_opt)))]
147 warehouse: Option<String>,
148
149 #[builder(default)]
150 props: HashMap<String, String>,
151
152 #[builder(default)]
153 client: Option<Client>,
154
155 #[builder(default)]
159 default_client: Arc<OnceLock<Client>>,
160}
161
162fn is_sensitive_prop(key: &str) -> bool {
165 key.contains("token")
166 || key.contains("credential")
167 || key.contains("secret")
168 || key.contains("password")
169 || key.contains("key")
170 || key.contains("connection-string")
171 || key.starts_with("header.")
172}
173
174impl Debug for RestCatalogConfig {
177 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
178 let props: HashMap<&str, &str> = self
179 .props
180 .iter()
181 .map(|(key, value)| {
182 let value = if is_sensitive_prop(key) {
183 "[REDACTED]"
184 } else {
185 value.as_str()
186 };
187 (key.as_str(), value)
188 })
189 .collect();
190 f.debug_struct("RestCatalogConfig")
191 .field("name", &self.name)
192 .field("uri", &self.uri)
193 .field("warehouse", &self.warehouse)
194 .field("props", &props)
195 .finish_non_exhaustive()
196 }
197}
198
199impl RestCatalogConfig {
200 fn url_prefixed(&self, parts: &[&str]) -> String {
201 [&self.uri, PATH_V1]
202 .into_iter()
203 .chain(self.props.get("prefix").map(|s| &**s))
204 .chain(parts.iter().cloned())
205 .join("/")
206 }
207
208 fn config_endpoint(&self) -> String {
209 [&self.uri, PATH_V1, "config"].join("/")
210 }
211
212 pub(crate) fn get_token_endpoint(&self) -> String {
213 self.explicit_oauth2_server_uri()
214 .unwrap_or_else(|| default_token_endpoint(&self.uri))
215 }
216
217 pub(crate) fn explicit_oauth2_server_uri(&self) -> Option<String> {
219 self.props.get("oauth2-server-uri").cloned()
220 }
221
222 fn namespaces_endpoint(&self) -> String {
223 self.url_prefixed(&["namespaces"])
224 }
225
226 fn namespace_endpoint(&self, ns: &NamespaceIdent) -> String {
227 self.url_prefixed(&["namespaces", &ns.to_url_string()])
228 }
229
230 fn tables_endpoint(&self, ns: &NamespaceIdent) -> String {
231 self.url_prefixed(&["namespaces", &ns.to_url_string(), "tables"])
232 }
233
234 fn rename_table_endpoint(&self) -> String {
235 self.url_prefixed(&["tables", "rename"])
236 }
237
238 fn register_table_endpoint(&self, ns: &NamespaceIdent) -> String {
239 self.url_prefixed(&["namespaces", &ns.to_url_string(), "register"])
240 }
241
242 fn table_endpoint(&self, table: &TableIdent) -> String {
243 self.url_prefixed(&[
244 "namespaces",
245 &table.namespace.to_url_string(),
246 "tables",
247 &table.name,
248 ])
249 }
250
251 pub(crate) fn client(&self) -> Client {
255 self.client
256 .clone()
257 .unwrap_or_else(|| self.default_client.get_or_init(Client::default).clone())
258 }
259
260 pub(crate) fn token(&self) -> Option<String> {
264 self.props.get("token").cloned()
265 }
266
267 pub(crate) fn credential(&self) -> Option<(Option<String>, String)> {
270 credential_from_props(&self.props)
271 }
272
273 pub(crate) fn extra_headers(&self) -> Result<HeaderMap> {
275 extra_headers_from_props(&self.props)
276 }
277
278 pub(crate) fn extra_oauth_params(&self) -> HashMap<String, String> {
280 oauth_params_from_props(&self.props)
281 }
282
283 pub(crate) fn disable_header_redaction(&self) -> bool {
288 self.props
289 .get(REST_CATALOG_PROP_DISABLE_HEADER_REDACTION)
290 .map(|v| v.eq_ignore_ascii_case("true"))
291 .unwrap_or(false)
292 }
293
294 pub(crate) fn merge_with_config(mut self, mut config: CatalogConfig) -> Self {
296 if let Some(uri) = config.overrides.remove(REST_CATALOG_PROP_URI) {
297 self.uri = uri;
298 }
299
300 let mut props = config.defaults;
301 props.extend(self.props);
302 if let Some(warehouse) = &self.warehouse {
305 props.insert(REST_CATALOG_PROP_WAREHOUSE.to_string(), warehouse.clone());
306 }
307 props.extend(config.overrides);
308
309 self.props = props;
310 self
311 }
312}
313
314pub(crate) fn credential_from_props(
322 props: &HashMap<String, String>,
323) -> Option<(Option<String>, String)> {
324 let cred = props.get("credential")?;
325
326 match cred.split_once(':') {
327 Some((client_id, client_secret)) => {
328 Some((Some(client_id.to_string()), client_secret.to_string()))
329 }
330 None => Some((None, cred.to_string())),
331 }
332}
333
334pub(crate) fn extra_headers_from_props(props: &HashMap<String, String>) -> Result<HeaderMap> {
341 let mut headers = HeaderMap::from_iter([
342 (
343 header::CONTENT_TYPE,
344 HeaderValue::from_static("application/json"),
345 ),
346 (
347 HeaderName::from_static("x-client-version"),
348 HeaderValue::from_static(ICEBERG_REST_SPEC_VERSION),
349 ),
350 (
351 header::USER_AGENT,
352 HeaderValue::from_str(&format!("iceberg-rs/{CARGO_PKG_VERSION}")).unwrap(),
353 ),
354 ]);
355
356 headers.extend(explicit_headers_from_props(props)?);
357
358 Ok(headers)
359}
360
361pub(crate) fn default_token_endpoint(uri: &str) -> String {
363 [uri, PATH_V1, "oauth", "tokens"].join("/")
364}
365
366pub(crate) fn explicit_headers_from_props(props: &HashMap<String, String>) -> Result<HeaderMap> {
368 let mut headers = HeaderMap::new();
369 for (key, value) in props
370 .iter()
371 .filter_map(|(k, v)| k.strip_prefix("header.").map(|k| (k, v)))
372 {
373 headers.insert(
374 HeaderName::from_str(key).map_err(|e| {
375 Error::new(
376 ErrorKind::DataInvalid,
377 format!("Invalid header name: {key}"),
378 )
379 .with_source(e)
380 })?,
381 HeaderValue::from_str(value).map_err(|e| {
382 Error::new(
383 ErrorKind::DataInvalid,
384 format!("Invalid value for header: {key}"),
386 )
387 .with_source(e)
388 })?,
389 );
390 }
391
392 Ok(headers)
393}
394
395pub(crate) fn oauth_params_from_props(props: &HashMap<String, String>) -> HashMap<String, String> {
397 let mut params = HashMap::new();
398
399 if let Some(scope) = props.get("scope") {
400 params.insert("scope".to_string(), scope.to_string());
401 } else {
402 params.insert("scope".to_string(), "catalog".to_string());
403 }
404
405 let optional_params = ["audience", "resource"];
406 for param_name in optional_params {
407 if let Some(value) = props.get(param_name) {
408 params.insert(param_name.to_string(), value.to_string());
409 }
410 }
411
412 params
413}
414
415#[derive(Debug)]
416struct RestClient {
417 http_client: HttpClient,
420 config: RestCatalogConfig,
424 endpoints: HashSet<Endpoint>,
426}
427
428impl RestClient {
429 async fn init(
432 user_config: &RestCatalogConfig,
433 auth_manager: Arc<dyn AuthManager>,
434 ) -> Result<Self> {
435 let http_client = HttpClient::new(user_config)?;
436 let catalog_config = {
440 let init_session = auth_manager
441 .init_session(
442 &http_client.without_auth_session(),
443 &Self::auth_props(user_config),
444 )
445 .await?;
446 Self::load_config(
447 &http_client.with_auth_session(Arc::from(init_session)),
448 user_config,
449 )
450 .await?
451 };
452 let endpoints = match &catalog_config.endpoints {
455 Some(advertised) if !advertised.is_empty() => advertised.iter().cloned().collect(),
456 _ => crate::endpoint::DEFAULT_ENDPOINTS.clone(),
457 };
458 let config = user_config.clone().merge_with_config(catalog_config);
459 let http_client = http_client.update_with(&config)?;
460 let session = auth_manager
463 .catalog_session(
464 &http_client.without_auth_session(),
465 &Self::auth_props(&config),
466 )
467 .await?;
468
469 Ok(Self {
470 config,
471 http_client: http_client.with_auth_session(session),
472 endpoints,
473 })
474 }
475
476 #[cfg(test)]
478 async fn token(&self) -> Option<String> {
479 self.http_client.token().await
480 }
481
482 async fn query_catalog(&self, request: HttpRequest) -> Result<HttpResponse> {
484 self.http_client.query_catalog(request).await
485 }
486
487 fn auth_props(config: &RestCatalogConfig) -> HashMap<String, String> {
490 let mut props = config.props.clone();
495 props.insert(REST_CATALOG_PROP_URI.to_string(), config.uri.clone());
496 if let Some(warehouse) = &config.warehouse {
497 props
500 .entry(REST_CATALOG_PROP_WAREHOUSE.to_string())
501 .or_insert_with(|| warehouse.clone());
502 }
503 props
504 }
505
506 async fn load_config(
510 http_client: &HttpClient,
511 user_config: &RestCatalogConfig,
512 ) -> Result<CatalogConfig> {
513 let mut request_builder = http_client.request(Method::GET, user_config.config_endpoint());
514
515 if let Some(warehouse_location) = &user_config.warehouse {
516 request_builder = request_builder.query(&[("warehouse", warehouse_location)]);
517 }
518
519 let request = HttpRequest::build(request_builder)?;
520
521 let http_response = http_client.query_catalog(request).await?;
522
523 match http_response.status() {
524 StatusCode::OK => deserialize_catalog_response(http_response),
525 _ => Err(deserialize_unexpected_catalog_error(
526 http_response,
527 http_client.disable_header_redaction(),
528 )),
529 }
530 }
531}
532
533#[derive(Debug)]
539pub struct RestCatalog {
540 session_context: SessionContext,
541 inner: Arc<RestSessionCatalog>,
542}
543
544impl RestCatalog {
545 #[cfg(test)]
547 fn new(
548 context: SessionContext,
549 config: RestCatalogConfig,
550 auth_manager: Option<Arc<dyn AuthManager>>,
551 storage_factory: Option<Arc<dyn StorageFactory>>,
552 runtime: Runtime,
553 kms_client: Option<Arc<dyn KeyManagementClient>>,
554 ) -> Self {
555 let session_catalog = Arc::new(RestSessionCatalog::new(
556 config,
557 auth_manager,
558 storage_factory,
559 runtime,
560 kms_client,
561 ));
562
563 Self::from_session_catalog(context, session_catalog)
564 }
565
566 fn from_session_catalog(context: SessionContext, inner: Arc<RestSessionCatalog>) -> Self {
567 Self {
568 session_context: context,
569 inner,
570 }
571 }
572
573 #[cfg(test)]
574 async fn client(&self) -> Result<&RestClient> {
575 self.inner.client().await
576 }
577}
578
579#[async_trait]
583impl Catalog for RestCatalog {
584 async fn list_namespaces(
585 &self,
586 parent: Option<&NamespaceIdent>,
587 ) -> Result<Vec<NamespaceIdent>> {
588 self.inner
589 .list_namespaces(&self.session_context, parent)
590 .await
591 }
592
593 async fn create_namespace(
594 &self,
595 namespace: &NamespaceIdent,
596 properties: HashMap<String, String>,
597 ) -> Result<Namespace> {
598 self.inner
599 .create_namespace(&self.session_context, namespace, properties)
600 .await
601 }
602
603 async fn get_namespace(&self, namespace: &NamespaceIdent) -> Result<Namespace> {
604 self.inner
605 .get_namespace(&self.session_context, namespace)
606 .await
607 }
608
609 async fn namespace_exists(&self, ns: &NamespaceIdent) -> Result<bool> {
610 self.inner.namespace_exists(&self.session_context, ns).await
611 }
612
613 async fn update_namespace(
614 &self,
615 namespace: &NamespaceIdent,
616 properties: HashMap<String, String>,
617 ) -> Result<()> {
618 self.inner
619 .update_namespace(&self.session_context, namespace, properties)
620 .await
621 }
622
623 async fn drop_namespace(&self, namespace: &NamespaceIdent) -> Result<()> {
624 self.inner
625 .drop_namespace(&self.session_context, namespace)
626 .await
627 }
628
629 async fn list_tables(&self, namespace: &NamespaceIdent) -> Result<Vec<TableIdent>> {
630 self.inner
631 .list_tables(&self.session_context, namespace)
632 .await
633 }
634
635 async fn create_table(
636 &self,
637 namespace: &NamespaceIdent,
638 creation: TableCreation,
639 ) -> Result<Table> {
640 self.inner
641 .create_table(&self.session_context, namespace, creation)
642 .await
643 }
644
645 async fn load_table(&self, table_ident: &TableIdent) -> Result<Table> {
646 self.inner
647 .load_table(&self.session_context, table_ident)
648 .await
649 }
650
651 async fn drop_table(&self, table: &TableIdent) -> Result<()> {
652 self.inner.drop_table(&self.session_context, table).await
653 }
654
655 async fn purge_table(&self, table: &TableIdent) -> Result<()> {
656 self.inner.purge_table(&self.session_context, table).await
657 }
658
659 async fn table_exists(&self, table: &TableIdent) -> Result<bool> {
660 self.inner.table_exists(&self.session_context, table).await
661 }
662
663 async fn rename_table(&self, src: &TableIdent, dest: &TableIdent) -> Result<()> {
664 self.inner
665 .rename_table(&self.session_context, src, dest)
666 .await
667 }
668
669 async fn register_table(
670 &self,
671 table_ident: &TableIdent,
672 metadata_location: String,
673 ) -> Result<Table> {
674 self.inner
675 .register_table(&self.session_context, table_ident, metadata_location)
676 .await
677 }
678
679 async fn update_table(&self, commit: TableCommit) -> Result<Table> {
680 self.inner.update_table(&self.session_context, commit).await
681 }
682}
683
684#[derive(Debug)]
689pub struct RestSessionCatalog {
690 auth_manager: Option<Arc<dyn AuthManager>>,
693 user_config: RestCatalogConfig,
697 client: OnceCell<RestClient>,
698 storage_factory: Option<Arc<dyn StorageFactory>>,
700 runtime: Runtime,
701 kms_client: Option<Arc<dyn KeyManagementClient>>,
703}
704
705impl RestSessionCatalog {
706 fn new(
708 config: RestCatalogConfig,
709 auth_manager: Option<Arc<dyn AuthManager>>,
710 storage_factory: Option<Arc<dyn StorageFactory>>,
711 runtime: Runtime,
712 kms_client: Option<Arc<dyn KeyManagementClient>>,
713 ) -> Self {
714 Self {
715 auth_manager,
716 user_config: config,
717 client: OnceCell::new(),
718 storage_factory,
719 runtime,
720 kms_client,
721 }
722 }
723
724 async fn delete_table(
726 &self,
727 _context: &SessionContext,
728 table: &TableIdent,
729 purge: bool,
730 ) -> Result<()> {
731 let client = self.client().await?;
732
733 let mut request_builder = client
734 .http_client
735 .request(Method::DELETE, client.config.table_endpoint(table));
736
737 if purge {
738 request_builder = request_builder.query(&[("purgeRequested", "true")]);
739 }
740
741 let request = HttpRequest::build(request_builder)?;
742 let http_response = client.query_catalog(request).await?;
743
744 match http_response.status() {
745 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
746 StatusCode::NOT_FOUND => Err(Error::new(
747 ErrorKind::TableNotFound,
748 "Tried to drop a table that does not exist",
749 )),
750 _ => Err(deserialize_unexpected_catalog_error(
751 http_response,
752 client.http_client.disable_header_redaction(),
753 )),
754 }
755 }
756
757 fn auth_type(config: &RestCatalogConfig) -> String {
762 config
763 .props
764 .get(REST_CATALOG_PROP_AUTH_TYPE)
765 .map(|auth_type| auth_type.to_ascii_lowercase())
767 .unwrap_or_else(|| {
768 if config.token().is_some()
769 || config.credential().is_some()
770 || config.explicit_oauth2_server_uri().is_some()
771 {
772 AUTH_TYPE_OAUTH2.to_string()
773 } else {
774 AUTH_TYPE_NONE.to_string()
775 }
776 })
777 }
778
779 fn resolve_auth_manager(&self) -> Result<Arc<dyn AuthManager>> {
782 if let Some(auth_manager) = &self.auth_manager {
783 return Ok(auth_manager.clone());
784 }
785 let config = &self.user_config;
786 let auth_type = Self::auth_type(config);
787 if auth_type == AUTH_TYPE_OAUTH2 && !config.props.contains_key(REST_CATALOG_PROP_AUTH_TYPE)
790 {
791 tracing::warn!(
792 "Inferring {REST_CATALOG_PROP_AUTH_TYPE}={AUTH_TYPE_OAUTH2} from the configured \
793 OAuth properties; set it explicitly to avoid this warning"
794 );
795 }
796 match auth_type.as_str() {
797 AUTH_TYPE_NONE => Ok(Arc::new(NoopAuthManager)),
798 AUTH_TYPE_OAUTH2 => Ok(Arc::new(OAuth2Manager::from_config(config)?)),
799 other => Err(Error::new(
800 ErrorKind::DataInvalid,
801 format!(
802 "unknown '{REST_CATALOG_PROP_AUTH_TYPE}': {other}; use \
803 `RestSessionCatalogBuilder::with_auth_manager` or \
804 `RestCatalogBuilder::with_auth_manager` to inject a custom auth manager"
805 ),
806 )),
807 }
808 }
809
810 async fn client(&self) -> Result<&RestClient> {
812 self.client
813 .get_or_try_init(|| async {
814 RestClient::init(&self.user_config, self.resolve_auth_manager()?).await
815 })
816 .await
817 }
818
819 pub(crate) async fn supports_endpoint(&self, endpoint: &Endpoint) -> Result<bool> {
823 Ok(self.client().await?.endpoints.contains(endpoint))
824 }
825
826 async fn check_exists_via_head(&self, client: &RestClient, url: String) -> Result<bool> {
829 let request = HttpRequest::build(client.http_client.request(Method::HEAD, url))?;
830 let http_response = client.query_catalog(request).await?;
831
832 match http_response.status() {
833 StatusCode::NO_CONTENT | StatusCode::OK => Ok(true),
834 StatusCode::NOT_FOUND => Ok(false),
835 _ => Err(deserialize_unexpected_catalog_error(
836 http_response,
837 client.http_client.disable_header_redaction(),
838 )),
839 }
840 }
841
842 async fn load_file_io(
843 &self,
844 metadata_location: Option<&str>,
845 extra_config: Option<HashMap<String, String>>,
846 ) -> Result<FileIO> {
847 let mut props = self.client().await?.config.props.clone();
848 if let Some(config) = extra_config {
849 props.extend(config);
850 }
851
852 let warehouse_path = match self.client().await?.config.warehouse.as_deref() {
855 Some(url) if Url::parse(url).is_ok() => Some(url),
856 Some(_) => None,
857 None => None,
858 };
859
860 if metadata_location.or(warehouse_path).is_none() {
861 return Err(Error::new(
862 ErrorKind::Unexpected,
863 "Unable to load file io, neither warehouse nor metadata location is set!",
864 ));
865 }
866
867 let factory = self
869 .storage_factory
870 .clone()
871 .ok_or_else(|| {
872 Error::new(
873 ErrorKind::Unexpected,
874 "StorageFactory must be provided for REST catalog table operations. Use `with_storage_factory` to configure it.",
875 )
876 })?;
877
878 let file_io = FileIOBuilder::new(factory).with_props(props).build();
879
880 Ok(file_io)
881 }
882}
883
884#[async_trait]
887impl SessionCatalog for RestSessionCatalog {
888 async fn list_namespaces(
889 &self,
890 _context: &SessionContext,
891 parent: Option<&NamespaceIdent>,
892 ) -> Result<Vec<NamespaceIdent>> {
893 let client = self.client().await?;
894 let endpoint = client.config.namespaces_endpoint();
895 let mut namespaces = Vec::new();
896 let mut next_token = None;
897
898 loop {
899 let mut request = client.http_client.request(Method::GET, endpoint.clone());
900
901 if let Some(ns) = parent {
903 request = request.query(&[("parent", ns.to_url_string())]);
904 }
905
906 if let Some(token) = next_token {
907 request = request.query(&[("pageToken", token)]);
908 }
909
910 let http_response = client.query_catalog(HttpRequest::build(request)?).await?;
911
912 match http_response.status() {
913 StatusCode::OK => {
914 let response =
915 deserialize_catalog_response::<ListNamespaceResponse>(http_response)?;
916
917 namespaces.extend(response.namespaces);
918
919 match response.next_page_token {
920 Some(token) => next_token = Some(token),
921 None => break,
922 }
923 }
924 StatusCode::NOT_FOUND => {
925 return Err(Error::new(
926 ErrorKind::NamespaceNotFound,
927 "The parent parameter of the namespace provided does not exist",
928 ));
929 }
930 _ => {
931 return Err(deserialize_unexpected_catalog_error(
932 http_response,
933 client.http_client.disable_header_redaction(),
934 ));
935 }
936 }
937 }
938
939 Ok(namespaces)
940 }
941
942 async fn create_namespace(
943 &self,
944 _context: &SessionContext,
945 namespace: &NamespaceIdent,
946 properties: HashMap<String, String>,
947 ) -> Result<Namespace> {
948 let client = self.client().await?;
949
950 let request = HttpRequest::build(
951 client
952 .http_client
953 .request(Method::POST, client.config.namespaces_endpoint())
954 .json(&CreateNamespaceRequest {
955 namespace: namespace.clone(),
956 properties,
957 }),
958 )?;
959
960 let http_response = client.query_catalog(request).await?;
961
962 match http_response.status() {
963 StatusCode::OK => {
964 let response = deserialize_catalog_response::<NamespaceResponse>(http_response)?;
965 Ok(Namespace::from(response))
966 }
967 StatusCode::CONFLICT => Err(Error::new(
968 ErrorKind::NamespaceAlreadyExists,
969 "Tried to create a namespace that already exists",
970 )),
971 _ => Err(deserialize_unexpected_catalog_error(
972 http_response,
973 client.http_client.disable_header_redaction(),
974 )),
975 }
976 }
977
978 async fn get_namespace(
979 &self,
980 _context: &SessionContext,
981 namespace: &NamespaceIdent,
982 ) -> Result<Namespace> {
983 let client = self.client().await?;
984
985 let request = HttpRequest::build(
986 client
987 .http_client
988 .request(Method::GET, client.config.namespace_endpoint(namespace)),
989 )?;
990
991 let http_response = client.query_catalog(request).await?;
992
993 match http_response.status() {
994 StatusCode::OK => {
995 let response = deserialize_catalog_response::<NamespaceResponse>(http_response)?;
996 Ok(Namespace::from(response))
997 }
998 StatusCode::NOT_FOUND => Err(Error::new(
999 ErrorKind::NamespaceNotFound,
1000 "Tried to get a namespace that does not exist",
1001 )),
1002 _ => Err(deserialize_unexpected_catalog_error(
1003 http_response,
1004 client.http_client.disable_header_redaction(),
1005 )),
1006 }
1007 }
1008
1009 async fn namespace_exists(
1010 &self,
1011 context: &SessionContext,
1012 ns: &NamespaceIdent,
1013 ) -> Result<bool> {
1014 if !self.supports_endpoint(&V1_NAMESPACE_EXISTS).await? {
1019 return match self.get_namespace(context, ns).await {
1020 Ok(_) => Ok(true),
1021 Err(e) if e.kind() == ErrorKind::NamespaceNotFound => Ok(false),
1022 Err(e) => Err(e),
1023 };
1024 }
1025
1026 let client = self.client().await?;
1027 self.check_exists_via_head(client, client.config.namespace_endpoint(ns))
1028 .await
1029 }
1030
1031 async fn update_namespace(
1032 &self,
1033 _context: &SessionContext,
1034 _namespace: &NamespaceIdent,
1035 _properties: HashMap<String, String>,
1036 ) -> Result<()> {
1037 Err(Error::new(
1038 ErrorKind::FeatureUnsupported,
1039 "Updating namespace not supported yet!",
1040 ))
1041 }
1042
1043 async fn drop_namespace(
1044 &self,
1045 _context: &SessionContext,
1046 namespace: &NamespaceIdent,
1047 ) -> Result<()> {
1048 let client = self.client().await?;
1049
1050 let request = HttpRequest::build(
1051 client
1052 .http_client
1053 .request(Method::DELETE, client.config.namespace_endpoint(namespace)),
1054 )?;
1055
1056 let http_response = client.query_catalog(request).await?;
1057
1058 match http_response.status() {
1059 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
1060 StatusCode::NOT_FOUND => Err(Error::new(
1061 ErrorKind::NamespaceNotFound,
1062 "Tried to drop a namespace that does not exist",
1063 )),
1064 _ => Err(deserialize_unexpected_catalog_error(
1065 http_response,
1066 client.http_client.disable_header_redaction(),
1067 )),
1068 }
1069 }
1070
1071 async fn list_tables(
1072 &self,
1073 _context: &SessionContext,
1074 namespace: &NamespaceIdent,
1075 ) -> Result<Vec<TableIdent>> {
1076 let client = self.client().await?;
1077 let endpoint = client.config.tables_endpoint(namespace);
1078 let mut identifiers = Vec::new();
1079 let mut next_token = None;
1080
1081 loop {
1082 let mut request = client.http_client.request(Method::GET, endpoint.clone());
1083
1084 if let Some(token) = next_token {
1085 request = request.query(&[("pageToken", token)]);
1086 }
1087
1088 let http_response = client.query_catalog(HttpRequest::build(request)?).await?;
1089
1090 match http_response.status() {
1091 StatusCode::OK => {
1092 let response =
1093 deserialize_catalog_response::<ListTablesResponse>(http_response)?;
1094
1095 identifiers.extend(response.identifiers);
1096
1097 match response.next_page_token {
1098 Some(token) => next_token = Some(token),
1099 None => break,
1100 }
1101 }
1102 StatusCode::NOT_FOUND => {
1103 return Err(Error::new(
1104 ErrorKind::NamespaceNotFound,
1105 "Tried to list tables of a namespace that does not exist",
1106 ));
1107 }
1108 _ => {
1109 return Err(deserialize_unexpected_catalog_error(
1110 http_response,
1111 client.http_client.disable_header_redaction(),
1112 ));
1113 }
1114 }
1115 }
1116
1117 Ok(identifiers)
1118 }
1119
1120 async fn create_table(
1127 &self,
1128 _context: &SessionContext,
1129 namespace: &NamespaceIdent,
1130 creation: TableCreation,
1131 ) -> Result<Table> {
1132 let client = self.client().await?;
1133
1134 let table_ident = TableIdent::new(namespace.clone(), creation.name.clone());
1135
1136 let request = HttpRequest::build(
1137 client
1138 .http_client
1139 .request(Method::POST, client.config.tables_endpoint(namespace))
1140 .json(&CreateTableRequest {
1141 name: creation.name,
1142 location: creation.location,
1143 schema: creation.schema,
1144 partition_spec: creation.partition_spec,
1145 write_order: creation.sort_order,
1146 stage_create: Some(false),
1147 properties: creation.properties,
1148 }),
1149 )?;
1150
1151 let http_response = client.query_catalog(request).await?;
1152
1153 let response = match http_response.status() {
1154 StatusCode::OK => deserialize_catalog_response::<LoadTableResult>(http_response)?,
1155 StatusCode::NOT_FOUND => {
1156 return Err(Error::new(
1157 ErrorKind::NamespaceNotFound,
1158 "Tried to create a table under a namespace that does not exist",
1159 ));
1160 }
1161 StatusCode::CONFLICT => {
1162 return Err(Error::new(
1163 ErrorKind::TableAlreadyExists,
1164 "The table already exists",
1165 ));
1166 }
1167 _ => {
1168 return Err(deserialize_unexpected_catalog_error(
1169 http_response,
1170 client.http_client.disable_header_redaction(),
1171 ));
1172 }
1173 };
1174
1175 let metadata_location = response.metadata_location.as_ref().ok_or(Error::new(
1176 ErrorKind::DataInvalid,
1177 "Metadata location missing in `create_table` response!",
1178 ))?;
1179
1180 let config = response
1181 .config
1182 .into_iter()
1183 .chain(self.user_config.props.clone())
1184 .collect();
1185
1186 let file_io = self
1187 .load_file_io(Some(metadata_location), Some(config))
1188 .await?;
1189
1190 let mut table_builder = Table::builder()
1191 .identifier(table_ident.clone())
1192 .file_io(file_io)
1193 .metadata(response.metadata)
1194 .runtime(self.runtime.clone());
1195 if let Some(kms_client) = self.kms_client.clone() {
1196 table_builder = table_builder.kms_client(kms_client);
1197 }
1198
1199 if let Some(metadata_location) = response.metadata_location {
1200 table_builder.metadata_location(metadata_location).build()
1201 } else {
1202 table_builder.build()
1203 }
1204 }
1205
1206 async fn load_table(
1212 &self,
1213 _context: &SessionContext,
1214 table_ident: &TableIdent,
1215 ) -> Result<Table> {
1216 let client = self.client().await?;
1217
1218 let request = HttpRequest::build(
1219 client
1220 .http_client
1221 .request(Method::GET, client.config.table_endpoint(table_ident)),
1222 )?;
1223
1224 let http_response = client.query_catalog(request).await?;
1225
1226 let response = match http_response.status() {
1227 StatusCode::OK | StatusCode::NOT_MODIFIED => {
1228 deserialize_catalog_response::<LoadTableResult>(http_response)?
1229 }
1230 StatusCode::NOT_FOUND => {
1231 return Err(Error::new(
1232 ErrorKind::TableNotFound,
1233 "Tried to load a table that does not exist",
1234 ));
1235 }
1236 _ => {
1237 return Err(deserialize_unexpected_catalog_error(
1238 http_response,
1239 client.http_client.disable_header_redaction(),
1240 ));
1241 }
1242 };
1243
1244 let config = response
1245 .config
1246 .into_iter()
1247 .chain(self.user_config.props.clone())
1248 .collect();
1249
1250 let file_io = self
1251 .load_file_io(response.metadata_location.as_deref(), Some(config))
1252 .await?;
1253
1254 let mut table_builder = Table::builder()
1255 .identifier(table_ident.clone())
1256 .file_io(file_io)
1257 .metadata(response.metadata)
1258 .runtime(self.runtime.clone());
1259 if let Some(kms_client) = self.kms_client.clone() {
1260 table_builder = table_builder.kms_client(kms_client);
1261 }
1262
1263 if let Some(metadata_location) = response.metadata_location {
1264 table_builder.metadata_location(metadata_location).build()
1265 } else {
1266 table_builder.build()
1267 }
1268 }
1269
1270 async fn drop_table(&self, context: &SessionContext, table: &TableIdent) -> Result<()> {
1272 self.delete_table(context, table, false).await
1273 }
1274
1275 async fn purge_table(&self, context: &SessionContext, table: &TableIdent) -> Result<()> {
1278 self.delete_table(context, table, true).await
1279 }
1280
1281 async fn table_exists(&self, context: &SessionContext, table: &TableIdent) -> Result<bool> {
1283 if !self.supports_endpoint(&V1_TABLE_EXISTS).await? {
1287 return match self.load_table(context, table).await {
1288 Ok(_) => Ok(true),
1289 Err(e) if e.kind() == ErrorKind::TableNotFound => Ok(false),
1290 Err(e) => Err(e),
1291 };
1292 }
1293
1294 let client = self.client().await?;
1295 self.check_exists_via_head(client, client.config.table_endpoint(table))
1296 .await
1297 }
1298
1299 async fn rename_table(
1301 &self,
1302 _context: &SessionContext,
1303 src: &TableIdent,
1304 dest: &TableIdent,
1305 ) -> Result<()> {
1306 let client = self.client().await?;
1307
1308 let request = HttpRequest::build(
1309 client
1310 .http_client
1311 .request(Method::POST, client.config.rename_table_endpoint())
1312 .json(&RenameTableRequest {
1313 source: src.clone(),
1314 destination: dest.clone(),
1315 }),
1316 )?;
1317
1318 let http_response = client.query_catalog(request).await?;
1319
1320 match http_response.status() {
1321 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
1322 StatusCode::NOT_FOUND => Err(Error::new(
1323 ErrorKind::TableNotFound,
1324 "Tried to rename a table that does not exist (is the namespace correct?)",
1325 )),
1326 StatusCode::CONFLICT => Err(Error::new(
1327 ErrorKind::TableAlreadyExists,
1328 "Tried to rename a table to a name that already exists",
1329 )),
1330 _ => Err(deserialize_unexpected_catalog_error(
1331 http_response,
1332 client.http_client.disable_header_redaction(),
1333 )),
1334 }
1335 }
1336
1337 async fn register_table(
1338 &self,
1339 _context: &SessionContext,
1340 table_ident: &TableIdent,
1341 metadata_location: String,
1342 ) -> Result<Table> {
1343 let client = self.client().await?;
1344
1345 let request = HttpRequest::build(
1346 client
1347 .http_client
1348 .request(
1349 Method::POST,
1350 client
1351 .config
1352 .register_table_endpoint(table_ident.namespace()),
1353 )
1354 .json(&RegisterTableRequest {
1355 name: table_ident.name.clone(),
1356 metadata_location: metadata_location.clone(),
1357 overwrite: Some(false),
1358 }),
1359 )?;
1360
1361 let http_response = client.query_catalog(request).await?;
1362
1363 let response: LoadTableResult = match http_response.status() {
1364 StatusCode::OK => deserialize_catalog_response::<LoadTableResult>(http_response)?,
1365 StatusCode::NOT_FOUND => {
1366 return Err(Error::new(
1367 ErrorKind::NamespaceNotFound,
1368 "The namespace specified does not exist.",
1369 ));
1370 }
1371 StatusCode::CONFLICT => {
1372 return Err(Error::new(
1373 ErrorKind::TableAlreadyExists,
1374 "The given table already exists.",
1375 ));
1376 }
1377 _ => {
1378 return Err(deserialize_unexpected_catalog_error(
1379 http_response,
1380 client.http_client.disable_header_redaction(),
1381 ));
1382 }
1383 };
1384
1385 let metadata_location = response.metadata_location.as_ref().ok_or(Error::new(
1386 ErrorKind::DataInvalid,
1387 "Metadata location missing in `register_table` response!",
1388 ))?;
1389
1390 let file_io = self.load_file_io(Some(metadata_location), None).await?;
1391
1392 let mut table_builder = Table::builder()
1393 .identifier(table_ident.clone())
1394 .file_io(file_io)
1395 .metadata(response.metadata)
1396 .metadata_location(metadata_location.clone())
1397 .runtime(self.runtime.clone());
1398 if let Some(kms_client) = self.kms_client.clone() {
1399 table_builder = table_builder.kms_client(kms_client);
1400 }
1401 table_builder.build()
1402 }
1403
1404 async fn update_table(
1405 &self,
1406 _context: &SessionContext,
1407 mut commit: TableCommit,
1408 ) -> Result<Table> {
1409 let client = self.client().await?;
1410
1411 let request = HttpRequest::build(
1412 client
1413 .http_client
1414 .request(
1415 Method::POST,
1416 client.config.table_endpoint(commit.identifier()),
1417 )
1418 .json(&CommitTableRequest {
1419 identifier: Some(commit.identifier().clone()),
1420 requirements: commit.take_requirements(),
1421 updates: commit.take_updates(),
1422 }),
1423 )?;
1424
1425 let http_response = client.query_catalog(request).await?;
1426
1427 let response: CommitTableResponse = match http_response.status() {
1428 StatusCode::OK => deserialize_catalog_response(http_response)?,
1429 StatusCode::NOT_FOUND => {
1430 return Err(Error::new(
1431 ErrorKind::TableNotFound,
1432 "Tried to update a table that does not exist",
1433 ));
1434 }
1435 StatusCode::CONFLICT => {
1436 return Err(Error::new(
1437 ErrorKind::CatalogCommitConflicts,
1438 "CatalogCommitConflicts, one or more requirements failed. The client may retry.",
1439 )
1440 .with_retryable(true));
1441 }
1442 StatusCode::INTERNAL_SERVER_ERROR => {
1443 return Err(Error::new(
1444 ErrorKind::Unexpected,
1445 "An unknown server-side problem occurred; the commit state is unknown.",
1446 ));
1447 }
1448 StatusCode::BAD_GATEWAY => {
1449 return Err(Error::new(
1450 ErrorKind::Unexpected,
1451 "A gateway or proxy received an invalid response from the upstream server; the commit state is unknown.",
1452 ));
1453 }
1454 StatusCode::GATEWAY_TIMEOUT => {
1455 return Err(Error::new(
1456 ErrorKind::Unexpected,
1457 "A server-side gateway timeout occurred; the commit state is unknown.",
1458 ));
1459 }
1460 _ => {
1461 return Err(deserialize_unexpected_catalog_error(
1462 http_response,
1463 client.http_client.disable_header_redaction(),
1464 ));
1465 }
1466 };
1467
1468 let file_io = self
1469 .load_file_io(Some(&response.metadata_location), None)
1470 .await?;
1471
1472 let mut table_builder = Table::builder()
1473 .identifier(commit.identifier().clone())
1474 .file_io(file_io)
1475 .metadata(response.metadata)
1476 .metadata_location(response.metadata_location)
1477 .runtime(self.runtime.clone());
1478 if let Some(kms_client) = self.kms_client.clone() {
1479 table_builder = table_builder.kms_client(kms_client);
1480 }
1481 table_builder.build()
1482 }
1483}
1484
1485#[derive(Debug)]
1490pub struct RestSessionCatalogBuilder {
1491 config: RestCatalogConfig,
1492 auth_manager: Option<Arc<dyn AuthManager>>,
1493 storage_factory: Option<Arc<dyn StorageFactory>>,
1494 kms_client_factory: Option<Arc<dyn KmsClientFactory>>,
1495 runtime: Option<Runtime>,
1496}
1497
1498impl Default for RestSessionCatalogBuilder {
1499 fn default() -> Self {
1500 Self {
1501 config: RestCatalogConfig {
1502 name: None,
1503 uri: "".to_string(),
1504 warehouse: None,
1505 props: HashMap::new(),
1506 client: None,
1507 default_client: Arc::new(OnceLock::new()),
1508 },
1509 auth_manager: None,
1510 storage_factory: None,
1511 kms_client_factory: None,
1512 runtime: None,
1513 }
1514 }
1515}
1516
1517impl RestSessionCatalogBuilder {
1518 pub fn with_client(mut self, client: Client) -> Self {
1520 self.config.client = Some(client);
1521 self
1522 }
1523
1524 pub fn with_auth_manager(mut self, auth_manager: Arc<dyn AuthManager>) -> Self {
1526 self.auth_manager = Some(auth_manager);
1527 self
1528 }
1529
1530 pub fn with_storage_factory(mut self, storage_factory: Arc<dyn StorageFactory>) -> Self {
1555 self.storage_factory = Some(storage_factory);
1556 self
1557 }
1558
1559 pub fn with_kms_client_factory(
1580 mut self,
1581 kms_client_factory: Arc<dyn KmsClientFactory>,
1582 ) -> Self {
1583 self.kms_client_factory = Some(kms_client_factory);
1584 self
1585 }
1586
1587 pub fn with_runtime(mut self, runtime: Runtime) -> Self {
1593 self.runtime = Some(runtime);
1594 self
1595 }
1596
1597 pub fn load(
1602 mut self,
1603 name: impl Into<String>,
1604 props: HashMap<String, String>,
1605 ) -> impl Future<Output = Result<RestSessionCatalog>> + Send {
1606 self.config.name = Some(name.into());
1607
1608 if props.contains_key(REST_CATALOG_PROP_URI) {
1609 self.config.uri = props
1610 .get(REST_CATALOG_PROP_URI)
1611 .cloned()
1612 .unwrap_or_default();
1613 }
1614
1615 if props.contains_key(REST_CATALOG_PROP_WAREHOUSE) {
1616 self.config.warehouse = props.get(REST_CATALOG_PROP_WAREHOUSE).cloned()
1617 }
1618
1619 self.config.props = props
1621 .into_iter()
1622 .filter(|(k, _)| k != REST_CATALOG_PROP_URI && k != REST_CATALOG_PROP_WAREHOUSE)
1623 .collect();
1624
1625 async move {
1626 if self.config.name.is_none() {
1627 Err(Error::new(
1628 ErrorKind::DataInvalid,
1629 "Catalog name is required",
1630 ))
1631 } else if self.config.uri.is_empty() {
1632 Err(Error::new(
1633 ErrorKind::DataInvalid,
1634 "Catalog uri is required",
1635 ))
1636 } else {
1637 let runtime = self.runtime.unwrap_or_else(Runtime::current);
1638 let kms_client = match self.kms_client_factory {
1639 Some(factory) => Some(factory.create_kms_client(&self.config.props).await?),
1640 None => None,
1641 };
1642
1643 Ok(RestSessionCatalog::new(
1644 self.config,
1645 self.auth_manager,
1646 self.storage_factory,
1647 runtime,
1648 kms_client,
1649 ))
1650 }
1651 }
1652 }
1653}
1654
1655#[cfg(test)]
1656mod tests {
1657 use std::fs::File;
1658 use std::io::BufReader;
1659 use std::sync::Arc;
1660
1661 use chrono::{TimeZone, Utc};
1662 use iceberg::io::LocalFsStorageFactory;
1663 use iceberg::spec::{
1664 FormatVersion, NestedField, NullOrder, Operation, PrimitiveType, Schema, Snapshot,
1665 SnapshotLog, SortDirection, SortField, SortOrder, Summary, Transform, Type,
1666 UnboundPartitionField, UnboundPartitionSpec,
1667 };
1668 use iceberg::test_utils::test_runtime;
1669 use iceberg::transaction::{ApplyTransactionAction, Transaction};
1670 use mockito::{Mock, Server, ServerGuard};
1671 use serde_json::json;
1672 use uuid::uuid;
1673
1674 use super::*;
1675 use crate::auth::AuthSession;
1676 use crate::request::HttpRequest;
1677
1678 fn test_catalog(config: RestCatalogConfig) -> RestSessionCatalog {
1679 test_catalog_with(config, None)
1680 }
1681
1682 fn test_catalog_with(
1683 config: RestCatalogConfig,
1684 auth_manager: Option<Arc<dyn AuthManager>>,
1685 ) -> RestSessionCatalog {
1686 RestSessionCatalog::new(config, auth_manager, None, Runtime::current(), None)
1687 }
1688
1689 fn test_client() -> HttpClient {
1690 HttpClient::new(
1691 &RestCatalogConfig::builder()
1692 .uri("http://localhost".to_string())
1693 .build(),
1694 )
1695 .unwrap()
1696 }
1697
1698 fn session_catalog(config: RestCatalogConfig) -> RestSessionCatalog {
1700 RestSessionCatalog::new(
1701 config,
1702 None,
1703 Some(Arc::new(LocalFsStorageFactory)),
1704 Runtime::current(),
1705 None,
1706 )
1707 }
1708
1709 #[tokio::test]
1710 async fn test_update_config() {
1711 let mut server = Server::new_async().await;
1712
1713 let config_mock = server
1714 .mock("GET", "/v1/config")
1715 .with_status(200)
1716 .with_body(
1717 r#"{
1718 "overrides": {
1719 "warehouse": "s3://iceberg-catalog"
1720 },
1721 "defaults": {}
1722 }"#,
1723 )
1724 .create_async()
1725 .await;
1726
1727 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
1728
1729 assert_eq!(
1730 catalog
1731 .client()
1732 .await
1733 .unwrap()
1734 .config
1735 .props
1736 .get("warehouse"),
1737 Some(&"s3://iceberg-catalog".to_string())
1738 );
1739
1740 config_mock.assert_async().await;
1741 }
1742
1743 async fn create_config_mock(server: &mut ServerGuard) -> Mock {
1744 server
1745 .mock("GET", "/v1/config")
1746 .with_status(200)
1747 .with_body(
1748 r#"{
1749 "overrides": {
1750 "warehouse": "s3://iceberg-catalog"
1751 },
1752 "defaults": {}
1753 }"#,
1754 )
1755 .create_async()
1756 .await
1757 }
1758
1759 async fn create_config_mock_with_exists_endpoints(server: &mut ServerGuard) -> Mock {
1762 server
1763 .mock("GET", "/v1/config")
1764 .with_status(200)
1765 .with_body(
1766 r#"{
1767 "overrides": { "warehouse": "s3://iceberg-catalog" },
1768 "defaults": {},
1769 "endpoints": [
1770 "HEAD /v1/{prefix}/namespaces/{namespace}",
1771 "HEAD /v1/{prefix}/namespaces/{namespace}/tables/{table}"
1772 ]
1773 }"#,
1774 )
1775 .create_async()
1776 .await
1777 }
1778
1779 #[tokio::test]
1780 async fn test_config_advertised_endpoints() {
1781 let mut server = Server::new_async().await;
1782
1783 let config_mock = server
1784 .mock("GET", "/v1/config")
1785 .with_status(200)
1786 .with_body(
1787 r#"{
1788 "overrides": {},
1789 "defaults": {},
1790 "endpoints": [
1791 "GET /v1/{prefix}/namespaces",
1792 "POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan"
1793 ]
1794 }"#,
1795 )
1796 .create_async()
1797 .await;
1798
1799 let catalog = RestCatalog::new(
1800 SessionContext::empty(),
1801 RestCatalogConfig::builder().uri(server.url()).build(),
1802 None,
1803 Some(Arc::new(LocalFsStorageFactory)),
1804 Runtime::current(),
1805 None,
1806 );
1807
1808 let plan = "POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan"
1809 .parse::<Endpoint>()
1810 .unwrap();
1811 assert!(catalog.inner.supports_endpoint(&plan).await.unwrap());
1812 let delete_ns = "DELETE /v1/{prefix}/namespaces/{namespace}"
1814 .parse::<Endpoint>()
1815 .unwrap();
1816 assert!(!catalog.inner.supports_endpoint(&delete_ns).await.unwrap());
1817
1818 config_mock.assert_async().await;
1819 }
1820
1821 #[tokio::test]
1822 async fn test_config_without_endpoints_falls_back_to_default_set() {
1823 let mut server = Server::new_async().await;
1824
1825 let config_mock = server
1826 .mock("GET", "/v1/config")
1827 .with_status(200)
1828 .with_body(r#"{ "overrides": {}, "defaults": {} }"#)
1829 .create_async()
1830 .await;
1831
1832 let catalog = RestCatalog::new(
1833 SessionContext::empty(),
1834 RestCatalogConfig::builder().uri(server.url()).build(),
1835 None,
1836 Some(Arc::new(LocalFsStorageFactory)),
1837 Runtime::current(),
1838 None,
1839 );
1840
1841 let load_table = "GET /v1/{prefix}/namespaces/{namespace}/tables/{table}"
1844 .parse::<Endpoint>()
1845 .unwrap();
1846 assert!(catalog.inner.supports_endpoint(&load_table).await.unwrap());
1847 let plan = "POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan"
1849 .parse::<Endpoint>()
1850 .unwrap();
1851 assert!(!catalog.inner.supports_endpoint(&plan).await.unwrap());
1852
1853 config_mock.assert_async().await;
1854 }
1855
1856 #[tokio::test]
1857 async fn test_config_with_empty_endpoints_falls_back_to_default_set() {
1858 let mut server = Server::new_async().await;
1859
1860 let config_mock = server
1863 .mock("GET", "/v1/config")
1864 .with_status(200)
1865 .with_body(r#"{ "overrides": {}, "defaults": {}, "endpoints": [] }"#)
1866 .create_async()
1867 .await;
1868
1869 let catalog = RestCatalog::new(
1870 SessionContext::empty(),
1871 RestCatalogConfig::builder().uri(server.url()).build(),
1872 None,
1873 Some(Arc::new(LocalFsStorageFactory)),
1874 Runtime::current(),
1875 None,
1876 );
1877
1878 let load_table = "GET /v1/{prefix}/namespaces/{namespace}/tables/{table}"
1879 .parse::<Endpoint>()
1880 .unwrap();
1881 assert!(catalog.inner.supports_endpoint(&load_table).await.unwrap());
1882
1883 config_mock.assert_async().await;
1884 }
1885
1886 async fn create_oauth_mock(server: &mut ServerGuard) -> Mock {
1887 create_oauth_mock_with_path(server, "/v1/oauth/tokens", "ey000000000000", 200).await
1888 }
1889
1890 async fn create_oauth_mock_with_path(
1891 server: &mut ServerGuard,
1892 path: &str,
1893 token: &str,
1894 status: usize,
1895 ) -> Mock {
1896 let body = format!(
1897 r#"{{
1898 "access_token": "{token}",
1899 "token_type": "Bearer",
1900 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1901 "expires_in": 86400
1902 }}"#
1903 );
1904 server
1905 .mock("POST", path)
1906 .with_status(status)
1907 .with_body(body)
1908 .expect(1)
1909 .create_async()
1910 .await
1911 }
1912
1913 #[tokio::test]
1914 async fn test_oauth() {
1915 let mut server = Server::new_async().await;
1916 let oauth_mock = create_oauth_mock(&mut server).await;
1917 let config_mock = create_config_mock(&mut server).await;
1918
1919 let mut props = HashMap::new();
1920 props.insert("credential".to_string(), "client1:secret1".to_string());
1921
1922 let catalog = session_catalog(
1923 RestCatalogConfig::builder()
1924 .uri(server.url())
1925 .props(props)
1926 .build(),
1927 );
1928
1929 let token = catalog.client().await.unwrap().token().await;
1930 oauth_mock.assert_async().await;
1931 config_mock.assert_async().await;
1932 assert_eq!(token, Some("ey000000000000".to_string()));
1933 }
1934
1935 #[tokio::test]
1936 async fn test_oauth_with_optional_param() {
1937 let mut props = HashMap::new();
1938 props.insert("credential".to_string(), "client1:secret1".to_string());
1939 props.insert("scope".to_string(), "custom_scope".to_string());
1940 props.insert("audience".to_string(), "custom_audience".to_string());
1941 props.insert("resource".to_string(), "custom_resource".to_string());
1942
1943 let mut server = Server::new_async().await;
1944 let oauth_mock = server
1945 .mock("POST", "/v1/oauth/tokens")
1946 .match_body(mockito::Matcher::Regex("scope=custom_scope".to_string()))
1947 .match_body(mockito::Matcher::Regex(
1948 "audience=custom_audience".to_string(),
1949 ))
1950 .match_body(mockito::Matcher::Regex(
1951 "resource=custom_resource".to_string(),
1952 ))
1953 .with_status(200)
1954 .with_body(
1955 r#"{
1956 "access_token": "ey000000000000",
1957 "token_type": "Bearer",
1958 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1959 "expires_in": 86400
1960 }"#,
1961 )
1962 .expect(1)
1963 .create_async()
1964 .await;
1965
1966 let config_mock = create_config_mock(&mut server).await;
1967
1968 let catalog = session_catalog(
1969 RestCatalogConfig::builder()
1970 .uri(server.url())
1971 .props(props)
1972 .build(),
1973 );
1974
1975 let token = catalog.client().await.unwrap().token().await;
1976
1977 oauth_mock.assert_async().await;
1978 config_mock.assert_async().await;
1979 assert_eq!(token, Some("ey000000000000".to_string()));
1980 }
1981
1982 #[tokio::test]
1983 async fn test_http_headers() {
1984 let server = Server::new_async().await;
1985 let mut props = HashMap::new();
1986 props.insert("credential".to_string(), "client1:secret1".to_string());
1987
1988 let config = RestCatalogConfig::builder()
1989 .uri(server.url())
1990 .props(props)
1991 .build();
1992 let headers: HeaderMap = config.extra_headers().unwrap();
1993
1994 let expected_headers = HeaderMap::from_iter([
1995 (
1996 header::CONTENT_TYPE,
1997 HeaderValue::from_static("application/json"),
1998 ),
1999 (
2000 HeaderName::from_static("x-client-version"),
2001 HeaderValue::from_static(ICEBERG_REST_SPEC_VERSION),
2002 ),
2003 (
2004 header::USER_AGENT,
2005 HeaderValue::from_str(&format!("iceberg-rs/{CARGO_PKG_VERSION}")).unwrap(),
2006 ),
2007 ]);
2008 assert_eq!(headers, expected_headers);
2009 }
2010
2011 #[tokio::test]
2012 async fn test_http_headers_with_custom_headers() {
2013 let server = Server::new_async().await;
2014 let mut props = HashMap::new();
2015 props.insert("credential".to_string(), "client1:secret1".to_string());
2016 props.insert(
2017 "header.content-type".to_string(),
2018 "application/yaml".to_string(),
2019 );
2020 props.insert(
2021 "header.customized-header".to_string(),
2022 "some/value".to_string(),
2023 );
2024
2025 let config = RestCatalogConfig::builder()
2026 .uri(server.url())
2027 .props(props)
2028 .build();
2029 let headers: HeaderMap = config.extra_headers().unwrap();
2030
2031 let expected_headers = HeaderMap::from_iter([
2032 (
2033 header::CONTENT_TYPE,
2034 HeaderValue::from_static("application/yaml"),
2035 ),
2036 (
2037 HeaderName::from_static("x-client-version"),
2038 HeaderValue::from_static(ICEBERG_REST_SPEC_VERSION),
2039 ),
2040 (
2041 header::USER_AGENT,
2042 HeaderValue::from_str(&format!("iceberg-rs/{CARGO_PKG_VERSION}")).unwrap(),
2043 ),
2044 (
2045 HeaderName::from_static("customized-header"),
2046 HeaderValue::from_static("some/value"),
2047 ),
2048 ]);
2049 assert_eq!(headers, expected_headers);
2050 }
2051
2052 #[tokio::test]
2053 async fn test_oauth_with_oauth2_server_uri() {
2054 let mut server = Server::new_async().await;
2055 let config_mock = create_config_mock(&mut server).await;
2056
2057 let mut auth_server = Server::new_async().await;
2058 let auth_server_path = "/some/path";
2059 let oauth_mock =
2060 create_oauth_mock_with_path(&mut auth_server, auth_server_path, "ey000000000000", 200)
2061 .await;
2062
2063 let mut props = HashMap::new();
2064 props.insert("credential".to_string(), "client1:secret1".to_string());
2065 props.insert(
2066 "oauth2-server-uri".to_string(),
2067 format!("{}{}", auth_server.url(), auth_server_path).to_string(),
2068 );
2069
2070 let catalog = session_catalog(
2071 RestCatalogConfig::builder()
2072 .uri(server.url())
2073 .props(props)
2074 .build(),
2075 );
2076
2077 let token = catalog.client().await.unwrap().token().await;
2078
2079 oauth_mock.assert_async().await;
2080 config_mock.assert_async().await;
2081 assert_eq!(token, Some("ey000000000000".to_string()));
2082 }
2083
2084 #[tokio::test]
2085 async fn test_config_override() {
2086 let mut server = Server::new_async().await;
2087 let mut redirect_server = Server::new_async().await;
2088 let new_uri = redirect_server.url();
2089
2090 let config_mock = server
2091 .mock("GET", "/v1/config")
2092 .with_status(200)
2093 .with_body(
2094 json!(
2095 {
2096 "overrides": {
2097 "uri": new_uri,
2098 "warehouse": "s3://iceberg-catalog",
2099 "prefix": "ice/warehouses/my"
2100 },
2101 "defaults": {},
2102 }
2103 )
2104 .to_string(),
2105 )
2106 .create_async()
2107 .await;
2108
2109 let list_ns_mock = redirect_server
2110 .mock("GET", "/v1/ice/warehouses/my/namespaces")
2111 .with_body(
2112 r#"{
2113 "namespaces": []
2114 }"#,
2115 )
2116 .create_async()
2117 .await;
2118
2119 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
2120
2121 let _namespaces = catalog
2122 .list_namespaces(&SessionContext::empty(), None)
2123 .await
2124 .unwrap();
2125
2126 config_mock.assert_async().await;
2127 list_ns_mock.assert_async().await;
2128 }
2129
2130 #[tokio::test]
2131 async fn test_list_namespace() {
2132 let mut server = Server::new_async().await;
2133
2134 let config_mock = create_config_mock(&mut server).await;
2135
2136 let list_ns_mock = server
2137 .mock("GET", "/v1/namespaces")
2138 .with_body(
2139 r#"{
2140 "namespaces": [
2141 ["ns1", "ns11"],
2142 ["ns2"]
2143 ]
2144 }"#,
2145 )
2146 .create_async()
2147 .await;
2148
2149 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
2150
2151 let namespaces = catalog
2152 .list_namespaces(&SessionContext::empty(), None)
2153 .await
2154 .unwrap();
2155
2156 let expected_ns = vec![
2157 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
2158 NamespaceIdent::from_vec(vec!["ns2".to_string()]).unwrap(),
2159 ];
2160
2161 assert_eq!(expected_ns, namespaces);
2162
2163 config_mock.assert_async().await;
2164 list_ns_mock.assert_async().await;
2165 }
2166
2167 #[tokio::test]
2168 async fn test_auth_type_none_disables_auth() {
2169 let props = HashMap::from([
2171 (REST_CATALOG_PROP_AUTH_TYPE.to_string(), "none".to_string()),
2172 ("token".to_string(), "some-oauth-token".to_string()),
2173 ]);
2174 let config = RestCatalogConfig::builder()
2175 .uri("http://localhost".to_string())
2176 .props(props)
2177 .build();
2178
2179 let session = test_catalog(config)
2180 .resolve_auth_manager()
2181 .unwrap()
2182 .init_session(&test_client(), &HashMap::new())
2183 .await
2184 .unwrap();
2185 let mut req = HttpRequest::new(
2186 Client::new()
2187 .get("https://rest.example.com/v1/config")
2188 .build()
2189 .unwrap(),
2190 );
2191 session.authenticate(&mut req).await.unwrap();
2192 assert!(req.headers().get("authorization").is_none());
2193 }
2194
2195 #[tokio::test]
2196 async fn test_header_prop_overrides_token_on_the_wire() {
2197 let mut server = Server::new_async().await;
2201 let config_mock = create_config_mock(&mut server).await;
2202 let list_ns_mock = server
2203 .mock("GET", "/v1/namespaces")
2204 .match_header("authorization", "Basic xyz")
2205 .with_body(r#"{"namespaces": []}"#)
2206 .create_async()
2207 .await;
2208
2209 let props = HashMap::from([
2210 ("token".to_string(), "some-oauth-token".to_string()),
2211 ("header.authorization".to_string(), "Basic xyz".to_string()),
2212 ]);
2213 let catalog = RestCatalog::new(
2214 SessionContext::empty(),
2215 RestCatalogConfig::builder()
2216 .uri(server.url())
2217 .props(props)
2218 .build(),
2219 None,
2220 Some(Arc::new(LocalFsStorageFactory)),
2221 Runtime::current(),
2222 None,
2223 );
2224
2225 catalog.list_namespaces(None).await.unwrap();
2226 config_mock.assert_async().await;
2227 list_ns_mock.assert_async().await;
2228 }
2229
2230 #[tokio::test]
2231 async fn test_builtin_oauth_endpoint_follows_uri_override() {
2232 let mut bootstrap = Server::new_async().await;
2236 let overridden = Server::new_async().await;
2237
2238 let config_mock = bootstrap
2239 .mock("GET", "/v1/config")
2240 .with_status(200)
2241 .with_body(format!(
2242 r#"{{"overrides": {{"uri": "{}"}}, "defaults": {{}}}}"#,
2243 overridden.url()
2244 ))
2245 .create_async()
2246 .await;
2247 let bootstrap_oauth_mock =
2249 create_oauth_mock_with_path(&mut bootstrap, "/v1/oauth/tokens", "tok-boot", 200).await;
2250
2251 let props = HashMap::from([("credential".to_string(), "client1:secret1".to_string())]);
2252 let catalog = RestCatalog::new(
2253 SessionContext::empty(),
2254 RestCatalogConfig::builder()
2255 .uri(bootstrap.url())
2256 .props(props)
2257 .build(),
2258 None,
2259 Some(Arc::new(LocalFsStorageFactory)),
2260 Runtime::current(),
2261 None,
2262 );
2263
2264 let client = catalog.client().await.unwrap();
2265 config_mock.assert_async().await;
2266 bootstrap_oauth_mock.assert_async().await;
2267 let session_debug = format!("{:?}", client.http_client.auth_session());
2270 assert!(session_debug.contains(&format!("{}/v1/oauth/tokens", overridden.url())));
2271 }
2272
2273 #[tokio::test]
2274 async fn test_concurrent_authenticate_single_token_exchange() {
2275 let mut server = Server::new_async().await;
2278 let oauth_mock =
2280 create_oauth_mock_with_path(&mut server, "/v1/oauth/tokens", "tok-once", 200).await;
2281
2282 let manager = OAuth2Manager::new(format!("{}/v1/oauth/tokens", server.url()))
2283 .with_credential(Some("client1".to_string()), "secret1".to_string());
2284 let session: Arc<dyn AuthSession> = Arc::from(
2285 manager
2286 .init_session(&test_client(), &HashMap::new())
2287 .await
2288 .unwrap(),
2289 );
2290
2291 let client = Client::new();
2292 let attempts = (0..8).map(|_| {
2293 let session = session.clone();
2294 let client = client.clone();
2295 async move {
2296 let mut req = HttpRequest::new(
2297 client
2298 .get("https://rest.example.com/v1/config")
2299 .build()
2300 .unwrap(),
2301 );
2302 session.authenticate(&mut req).await.unwrap();
2303 req.headers()
2304 .get("authorization")
2305 .unwrap()
2306 .to_str()
2307 .unwrap()
2308 .to_string()
2309 }
2310 });
2311 let bearers = futures::future::join_all(attempts).await;
2312
2313 oauth_mock.assert_async().await;
2314 assert!(bearers.iter().all(|b| b == "Bearer tok-once"));
2315 }
2316
2317 #[tokio::test]
2318 async fn test_seeded_token_takes_precedence_over_credential() {
2319 let mut server = Server::new_async().await;
2322 let oauth_mock = server
2323 .mock("POST", "/v1/oauth/tokens")
2324 .expect(0)
2325 .create_async()
2326 .await;
2327
2328 let manager = OAuth2Manager::new(format!("{}/v1/oauth/tokens", server.url()))
2329 .with_token("tok-seed")
2330 .with_credential(Some("client1".to_string()), "secret1".to_string());
2331 let session = manager
2332 .init_session(&test_client(), &HashMap::new())
2333 .await
2334 .unwrap();
2335
2336 let mut req = HttpRequest::new(
2337 Client::new()
2338 .get("https://rest.example.com/v1/config")
2339 .build()
2340 .unwrap(),
2341 );
2342 session.authenticate(&mut req).await.unwrap();
2343 assert_eq!(
2344 req.headers().get("authorization").unwrap(),
2345 "Bearer tok-seed"
2346 );
2347
2348 oauth_mock.assert_async().await;
2349 }
2350
2351 #[tokio::test]
2352 async fn test_injected_oauth_manager_keeps_endpoint_and_options() {
2353 let mut server = Server::new_async().await;
2357 let config_mock = server
2360 .mock("GET", "/v1/config")
2361 .with_status(200)
2362 .with_body(r#"{"defaults": {"credential": "client1:secret1"}, "overrides": {}}"#)
2363 .create_async()
2364 .await;
2365
2366 let default_endpoint_mock = server
2368 .mock("POST", "/v1/oauth/tokens")
2369 .expect(0)
2370 .create_async()
2371 .await;
2372 let custom_endpoint_mock = server
2375 .mock("POST", "/custom/oauth/tokens")
2376 .match_header("x-tenant", "t1")
2377 .match_body(mockito::Matcher::AllOf(vec![
2380 mockito::Matcher::Regex("scope=catalog".to_string()),
2381 mockito::Matcher::Regex("audience=aud-1".to_string()),
2382 ]))
2383 .with_status(200)
2384 .with_body(
2385 r#"{
2386 "access_token": "ey000000000000",
2387 "token_type": "Bearer",
2388 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2389 "expires_in": 86400
2390 }"#,
2391 )
2392 .create_async()
2393 .await;
2394 let ns_mock = server
2395 .mock("GET", "/v1/namespaces")
2396 .match_header("authorization", "Bearer ey000000000000")
2397 .with_body(r#"{"namespaces": []}"#)
2398 .create_async()
2399 .await;
2400
2401 let manager = OAuth2Manager::new(format!("{}/custom/oauth/tokens", server.url()))
2402 .with_extra_headers(HeaderMap::from_iter([(
2403 HeaderName::from_static("x-tenant"),
2404 HeaderValue::from_static("t1"),
2405 )]))
2406 .with_extra_oauth_params(HashMap::from([(
2407 "audience".to_string(),
2408 "aud-1".to_string(),
2409 )]));
2410 let catalog = RestCatalog::new(
2411 SessionContext::empty(),
2412 RestCatalogConfig::builder().uri(server.url()).build(),
2413 Some(Arc::new(manager)),
2414 Some(Arc::new(LocalFsStorageFactory)),
2415 Runtime::current(),
2416 None,
2417 );
2418
2419 catalog.list_namespaces(None).await.unwrap();
2420
2421 config_mock.assert_async().await;
2422 custom_endpoint_mock.assert_async().await;
2423 default_endpoint_mock.assert_async().await;
2424 ns_mock.assert_async().await;
2425 }
2426
2427 #[tokio::test]
2428 async fn test_props_token_takes_precedence_over_props_credential() {
2429 let mut server = Server::new_async().await;
2431 let oauth_mock = server
2432 .mock("POST", "/v1/oauth/tokens")
2433 .expect(0)
2434 .create_async()
2435 .await;
2436
2437 let manager = OAuth2Manager::new(format!("{}/v1/oauth/tokens", server.url()));
2438 let session = manager
2439 .init_session(
2440 &test_client(),
2441 &HashMap::from([
2442 ("token".to_string(), "tok-props".to_string()),
2443 ("credential".to_string(), "client1:secret1".to_string()),
2444 ]),
2445 )
2446 .await
2447 .unwrap();
2448
2449 let mut req = HttpRequest::new(
2450 Client::new()
2451 .get("https://rest.example.com/v1/config")
2452 .build()
2453 .unwrap(),
2454 );
2455 session.authenticate(&mut req).await.unwrap();
2456 assert_eq!(
2457 req.headers().get("authorization").unwrap(),
2458 "Bearer tok-props"
2459 );
2460 oauth_mock.assert_async().await;
2461 }
2462
2463 #[tokio::test]
2464 async fn test_manager_exchanges_over_the_catalog_client() {
2465 let mut server = Server::new_async().await;
2469 let token_mock = server
2470 .mock("POST", "/v1/oauth/tokens")
2471 .match_header("x-from", "manager")
2472 .match_header("x-catalog-only", mockito::Matcher::Missing)
2473 .with_status(200)
2474 .with_body(r#"{"access_token": "tok", "token_type": "Bearer"}"#)
2475 .create_async()
2476 .await;
2477
2478 let catalog_client = HttpClient::new(
2479 &RestCatalogConfig::builder()
2480 .uri(server.url())
2481 .props(HashMap::from([(
2482 "header.x-catalog-only".to_string(),
2483 "not-on-token-requests".to_string(),
2484 )]))
2485 .build(),
2486 )
2487 .unwrap();
2488
2489 let manager = OAuth2Manager::new(format!("{}/v1/oauth/tokens", server.url()))
2490 .with_credential(Some("client1".to_string()), "secret1".to_string())
2491 .with_extra_headers(HeaderMap::from_iter([(
2492 HeaderName::from_static("x-from"),
2493 HeaderValue::from_static("manager"),
2494 )]));
2495 let session = manager
2496 .init_session(&catalog_client, &HashMap::new())
2497 .await
2498 .unwrap();
2499
2500 let mut req = HttpRequest::new(
2501 Client::new()
2502 .get("https://rest.example.com/v1/namespaces")
2503 .build()
2504 .unwrap(),
2505 );
2506 session.authenticate(&mut req).await.unwrap();
2507 token_mock.assert_async().await;
2508 }
2509
2510 #[tokio::test]
2511 async fn test_handshake_is_authenticated_by_the_init_session() {
2512 let mut server = Server::new_async().await;
2515 let config_mock = server
2516 .mock("GET", "/v1/config")
2517 .match_header("authorization", "Bearer tok-init")
2518 .with_status(200)
2519 .with_body(r#"{"defaults": {}, "overrides": {}}"#)
2520 .create_async()
2521 .await;
2522
2523 let catalog = RestCatalog::new(
2524 SessionContext::empty(),
2525 RestCatalogConfig::builder()
2526 .uri(server.url())
2527 .props(HashMap::from([(
2528 "token".to_string(),
2529 "tok-init".to_string(),
2530 )]))
2531 .build(),
2532 None,
2533 Some(Arc::new(LocalFsStorageFactory)),
2534 Runtime::current(),
2535 None,
2536 );
2537
2538 catalog.client().await.unwrap();
2539 config_mock.assert_async().await;
2540 }
2541
2542 #[tokio::test]
2543 async fn test_init_session_receives_user_props() {
2544 use tokio::sync::Mutex as AsyncMutex;
2545
2546 #[derive(Debug)]
2549 struct PlainSession;
2550 #[async_trait]
2551 impl AuthSession for PlainSession {
2552 async fn authenticate(&self, _request: &mut HttpRequest) -> Result<()> {
2553 Ok(())
2554 }
2555 }
2556
2557 #[derive(Debug)]
2558 struct CapturingManager(Arc<AsyncMutex<Option<HashMap<String, String>>>>);
2559 #[async_trait]
2560 impl AuthManager for CapturingManager {
2561 async fn init_session(
2562 &self,
2563 _client: &HttpClient,
2564 props: &HashMap<String, String>,
2565 ) -> Result<Box<dyn AuthSession>> {
2566 *self.0.lock().await = Some(props.clone());
2567 Ok(Box::new(PlainSession))
2568 }
2569 async fn catalog_session(
2570 &self,
2571 _client: &HttpClient,
2572 _props: &HashMap<String, String>,
2573 ) -> Result<Arc<dyn AuthSession>> {
2574 Ok(Arc::new(PlainSession))
2575 }
2576 }
2577
2578 let mut server = Server::new_async().await;
2579 let config_mock = create_config_mock(&mut server).await;
2580 let captured = Arc::new(AsyncMutex::new(None));
2581 let catalog = RestCatalog::new(
2582 SessionContext::empty(),
2583 RestCatalogConfig::builder()
2584 .uri(server.url())
2585 .props(HashMap::from([(
2586 "token".to_string(),
2587 "tok-user".to_string(),
2588 )]))
2589 .build(),
2590 Some(Arc::new(CapturingManager(captured.clone()))),
2591 Some(Arc::new(LocalFsStorageFactory)),
2592 Runtime::current(),
2593 None,
2594 );
2595
2596 catalog.client().await.unwrap();
2597 config_mock.assert_async().await;
2598 let props = captured.lock().await.clone().unwrap();
2599 assert_eq!(props.get("token").map(String::as_str), Some("tok-user"));
2600 assert_eq!(
2601 props.get(REST_CATALOG_PROP_URI).map(String::as_str),
2602 Some(server.url().as_str())
2603 );
2604 }
2605
2606 #[tokio::test]
2607 async fn test_catalog_session_receives_resolved_warehouse() {
2608 use tokio::sync::Mutex as AsyncMutex;
2609
2610 #[derive(Debug)]
2614 struct PlainSession;
2615 #[async_trait]
2616 impl AuthSession for PlainSession {
2617 async fn authenticate(&self, _request: &mut HttpRequest) -> Result<()> {
2618 Ok(())
2619 }
2620 }
2621
2622 #[derive(Debug)]
2623 struct CapturingManager(Arc<AsyncMutex<Option<HashMap<String, String>>>>);
2624 #[async_trait]
2625 impl AuthManager for CapturingManager {
2626 async fn init_session(
2627 &self,
2628 _client: &HttpClient,
2629 _props: &HashMap<String, String>,
2630 ) -> Result<Box<dyn AuthSession>> {
2631 Ok(Box::new(PlainSession))
2632 }
2633 async fn catalog_session(
2634 &self,
2635 _client: &HttpClient,
2636 props: &HashMap<String, String>,
2637 ) -> Result<Arc<dyn AuthSession>> {
2638 *self.0.lock().await = Some(props.clone());
2639 Ok(Arc::new(PlainSession))
2640 }
2641 }
2642
2643 let mut server = Server::new_async().await;
2645 let config_mock = server
2646 .mock("GET", "/v1/config")
2647 .match_query(mockito::Matcher::UrlEncoded(
2648 "warehouse".to_string(),
2649 "client-wh".to_string(),
2650 ))
2651 .with_status(200)
2652 .with_body(r#"{"defaults": {"warehouse": "default-wh"}, "overrides": {}}"#)
2653 .create_async()
2654 .await;
2655 let captured = Arc::new(AsyncMutex::new(None));
2656 let catalog = RestCatalog::new(
2657 SessionContext::empty(),
2658 RestCatalogConfig::builder()
2659 .uri(server.url())
2660 .warehouse("client-wh".to_string())
2661 .build(),
2662 Some(Arc::new(CapturingManager(captured.clone()))),
2663 Some(Arc::new(LocalFsStorageFactory)),
2664 Runtime::current(),
2665 None,
2666 );
2667 catalog.client().await.unwrap();
2668 config_mock.assert_async().await;
2669 let props = captured.lock().await.clone().unwrap();
2670 assert_eq!(
2671 props.get("warehouse").map(String::as_str),
2672 Some("client-wh")
2673 );
2674
2675 let mut server = Server::new_async().await;
2677 let config_mock = server
2678 .mock("GET", "/v1/config")
2679 .match_query(mockito::Matcher::UrlEncoded(
2680 "warehouse".to_string(),
2681 "client-wh".to_string(),
2682 ))
2683 .with_status(200)
2684 .with_body(r#"{"defaults": {}, "overrides": {"warehouse": "override-wh"}}"#)
2685 .create_async()
2686 .await;
2687 let captured = Arc::new(AsyncMutex::new(None));
2688 let catalog = RestCatalog::new(
2689 SessionContext::empty(),
2690 RestCatalogConfig::builder()
2691 .uri(server.url())
2692 .warehouse("client-wh".to_string())
2693 .build(),
2694 Some(Arc::new(CapturingManager(captured.clone()))),
2695 Some(Arc::new(LocalFsStorageFactory)),
2696 Runtime::current(),
2697 None,
2698 );
2699 catalog.client().await.unwrap();
2700 config_mock.assert_async().await;
2701 let props = captured.lock().await.clone().unwrap();
2702 assert_eq!(
2703 props.get("warehouse").map(String::as_str),
2704 Some("override-wh")
2705 );
2706 }
2707
2708 #[tokio::test]
2709 async fn test_init_session_dropped_before_catalog_session() {
2710 use std::sync::atomic::{AtomicBool, Ordering};
2711
2712 #[derive(Debug)]
2715 struct GuardSession(Arc<AtomicBool>);
2716 impl Drop for GuardSession {
2717 fn drop(&mut self) {
2718 self.0.store(true, Ordering::SeqCst);
2719 }
2720 }
2721 #[async_trait]
2722 impl AuthSession for GuardSession {
2723 async fn authenticate(&self, _request: &mut HttpRequest) -> Result<()> {
2724 Ok(())
2725 }
2726 }
2727
2728 #[derive(Debug)]
2729 struct PlainSession;
2730 #[async_trait]
2731 impl AuthSession for PlainSession {
2732 async fn authenticate(&self, _request: &mut HttpRequest) -> Result<()> {
2733 Ok(())
2734 }
2735 }
2736
2737 #[derive(Debug)]
2738 struct GuardManager(Arc<AtomicBool>);
2739 #[async_trait]
2740 impl AuthManager for GuardManager {
2741 async fn init_session(
2742 &self,
2743 _client: &HttpClient,
2744 _props: &HashMap<String, String>,
2745 ) -> Result<Box<dyn AuthSession>> {
2746 Ok(Box::new(GuardSession(self.0.clone())))
2747 }
2748 async fn catalog_session(
2749 &self,
2750 _client: &HttpClient,
2751 _props: &HashMap<String, String>,
2752 ) -> Result<Arc<dyn AuthSession>> {
2753 if !self.0.load(Ordering::SeqCst) {
2754 return Err(Error::new(
2755 ErrorKind::Unexpected,
2756 "init session must be dropped before catalog_session",
2757 ));
2758 }
2759 Ok(Arc::new(PlainSession))
2760 }
2761 }
2762
2763 let mut server = Server::new_async().await;
2764 let config_mock = create_config_mock(&mut server).await;
2765
2766 let dropped = Arc::new(AtomicBool::new(false));
2767 let catalog = RestCatalog::new(
2768 SessionContext::empty(),
2769 RestCatalogConfig::builder().uri(server.url()).build(),
2770 Some(Arc::new(GuardManager(dropped.clone()))),
2771 Some(Arc::new(LocalFsStorageFactory)),
2772 Runtime::current(),
2773 None,
2774 );
2775
2776 catalog.client().await.unwrap();
2777 config_mock.assert_async().await;
2778 assert!(dropped.load(Ordering::SeqCst));
2779 }
2780
2781 #[test]
2782 fn test_config_debug_redacts_secrets() {
2783 let config = RestCatalogConfig::builder()
2784 .uri("http://localhost".to_string())
2785 .props(HashMap::from([
2786 ("token".to_string(), "tok-secret".to_string()),
2787 ("credential".to_string(), "id:cred-secret".to_string()),
2788 ("header.authorization".to_string(), "Basic xyz".to_string()),
2789 ("adls.account-key".to_string(), "adls-secret".to_string()),
2790 ("s3.sse.key".to_string(), "sse-secret".to_string()),
2791 (
2792 "adls.connection-string".to_string(),
2793 "cs-secret".to_string(),
2794 ),
2795 ("warehouse".to_string(), "wh1".to_string()),
2796 ]))
2797 .build();
2798
2799 let out = format!("{config:?}");
2800 assert!(!out.contains("tok-secret"));
2801 assert!(!out.contains("cred-secret"));
2802 assert!(!out.contains("Basic xyz"));
2803 assert!(!out.contains("adls-secret"));
2804 assert!(!out.contains("sse-secret"));
2805 assert!(!out.contains("cs-secret"));
2806 assert!(out.contains("[REDACTED]"));
2807 assert!(out.contains("wh1"));
2808 }
2809
2810 #[tokio::test]
2811 async fn test_auth_type_defaults() {
2812 let bare = RestCatalogConfig::builder()
2815 .uri("http://localhost".to_string())
2816 .build();
2817 assert!(
2818 format!("{:?}", test_catalog(bare).resolve_auth_manager().unwrap())
2819 .contains("NoopAuthManager")
2820 );
2821
2822 let with_token = RestCatalogConfig::builder()
2823 .uri("http://localhost".to_string())
2824 .props(HashMap::from([("token".to_string(), "tok".to_string())]))
2825 .build();
2826 assert!(
2827 format!(
2828 "{:?}",
2829 test_catalog(with_token).resolve_auth_manager().unwrap()
2830 )
2831 .contains("OAuth2Manager")
2832 );
2833
2834 let mixed_case = RestCatalogConfig::builder()
2836 .uri("http://localhost".to_string())
2837 .props(HashMap::from([(
2838 REST_CATALOG_PROP_AUTH_TYPE.to_string(),
2839 "OAuth2".to_string(),
2840 )]))
2841 .build();
2842 assert!(
2843 format!(
2844 "{:?}",
2845 test_catalog(mixed_case).resolve_auth_manager().unwrap()
2846 )
2847 .contains("OAuth2Manager")
2848 );
2849
2850 let with_endpoint = RestCatalogConfig::builder()
2853 .uri("http://localhost".to_string())
2854 .props(HashMap::from([(
2855 "oauth2-server-uri".to_string(),
2856 "http://auth.example.com/tokens".to_string(),
2857 )]))
2858 .build();
2859 assert!(
2860 format!(
2861 "{:?}",
2862 test_catalog(with_endpoint).resolve_auth_manager().unwrap()
2863 )
2864 .contains("OAuth2Manager")
2865 );
2866 }
2867
2868 #[tokio::test]
2869 async fn test_unknown_auth_type_is_rejected() {
2870 let props = HashMap::from([(
2871 REST_CATALOG_PROP_AUTH_TYPE.to_string(),
2872 "kerberos".to_string(),
2873 )]);
2874 let config = RestCatalogConfig::builder()
2875 .uri("http://localhost".to_string())
2876 .props(props)
2877 .build();
2878
2879 let err = test_catalog(config).resolve_auth_manager().unwrap_err();
2880 assert!(err.message().contains(REST_CATALOG_PROP_AUTH_TYPE));
2881 }
2882
2883 #[tokio::test]
2884 async fn test_with_auth_manager_overrides_config() {
2885 #[derive(Debug)]
2887 struct StubAuthManager;
2888 #[async_trait]
2889 impl AuthManager for StubAuthManager {
2890 async fn init_session(
2891 &self,
2892 _client: &HttpClient,
2893 _props: &HashMap<String, String>,
2894 ) -> Result<Box<dyn AuthSession>> {
2895 unimplemented!()
2896 }
2897 async fn catalog_session(
2898 &self,
2899 _client: &HttpClient,
2900 _props: &HashMap<String, String>,
2901 ) -> Result<Arc<dyn AuthSession>> {
2902 unimplemented!()
2903 }
2904 }
2905
2906 let config = RestCatalogConfig::builder()
2907 .uri("http://localhost".to_string())
2908 .props(HashMap::from([(
2909 REST_CATALOG_PROP_AUTH_TYPE.to_string(),
2910 "kerberos".to_string(),
2911 )]))
2912 .build();
2913
2914 let catalog = test_catalog_with(config, Some(Arc::new(StubAuthManager)));
2916 assert!(catalog.resolve_auth_manager().is_ok());
2917 }
2918
2919 #[tokio::test]
2920 async fn test_list_namespace_with_pagination() {
2921 let mut server = Server::new_async().await;
2922
2923 let config_mock = create_config_mock(&mut server).await;
2924
2925 let list_ns_mock_page1 = server
2926 .mock("GET", "/v1/namespaces")
2927 .with_body(
2928 r#"{
2929 "namespaces": [
2930 ["ns1", "ns11"],
2931 ["ns2"]
2932 ],
2933 "next-page-token": "token123"
2934 }"#,
2935 )
2936 .create_async()
2937 .await;
2938
2939 let list_ns_mock_page2 = server
2940 .mock("GET", "/v1/namespaces?pageToken=token123")
2941 .with_body(
2942 r#"{
2943 "namespaces": [
2944 ["ns3"],
2945 ["ns4", "ns41"]
2946 ]
2947 }"#,
2948 )
2949 .create_async()
2950 .await;
2951
2952 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
2953
2954 let namespaces = catalog
2955 .list_namespaces(&SessionContext::empty(), None)
2956 .await
2957 .unwrap();
2958
2959 let expected_ns = vec![
2960 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
2961 NamespaceIdent::from_vec(vec!["ns2".to_string()]).unwrap(),
2962 NamespaceIdent::from_vec(vec!["ns3".to_string()]).unwrap(),
2963 NamespaceIdent::from_vec(vec!["ns4".to_string(), "ns41".to_string()]).unwrap(),
2964 ];
2965
2966 assert_eq!(expected_ns, namespaces);
2967
2968 config_mock.assert_async().await;
2969 list_ns_mock_page1.assert_async().await;
2970 list_ns_mock_page2.assert_async().await;
2971 }
2972
2973 #[tokio::test]
2974 async fn test_list_namespace_with_multiple_pages() {
2975 let mut server = Server::new_async().await;
2976
2977 let config_mock = create_config_mock(&mut server).await;
2978
2979 let list_ns_mock_page1 = server
2981 .mock("GET", "/v1/namespaces")
2982 .with_body(
2983 r#"{
2984 "namespaces": [
2985 ["ns1", "ns11"],
2986 ["ns2"]
2987 ],
2988 "next-page-token": "page2"
2989 }"#,
2990 )
2991 .create_async()
2992 .await;
2993
2994 let list_ns_mock_page2 = server
2996 .mock("GET", "/v1/namespaces?pageToken=page2")
2997 .with_body(
2998 r#"{
2999 "namespaces": [
3000 ["ns3"],
3001 ["ns4", "ns41"]
3002 ],
3003 "next-page-token": "page3"
3004 }"#,
3005 )
3006 .create_async()
3007 .await;
3008
3009 let list_ns_mock_page3 = server
3011 .mock("GET", "/v1/namespaces?pageToken=page3")
3012 .with_body(
3013 r#"{
3014 "namespaces": [
3015 ["ns5", "ns51", "ns511"]
3016 ],
3017 "next-page-token": "page4"
3018 }"#,
3019 )
3020 .create_async()
3021 .await;
3022
3023 let list_ns_mock_page4 = server
3025 .mock("GET", "/v1/namespaces?pageToken=page4")
3026 .with_body(
3027 r#"{
3028 "namespaces": [
3029 ["ns6"],
3030 ["ns7"]
3031 ],
3032 "next-page-token": "page5"
3033 }"#,
3034 )
3035 .create_async()
3036 .await;
3037
3038 let list_ns_mock_page5 = server
3040 .mock("GET", "/v1/namespaces?pageToken=page5")
3041 .with_body(
3042 r#"{
3043 "namespaces": [
3044 ["ns8", "ns81"]
3045 ]
3046 }"#,
3047 )
3048 .create_async()
3049 .await;
3050
3051 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3052
3053 let namespaces = catalog
3054 .list_namespaces(&SessionContext::empty(), None)
3055 .await
3056 .unwrap();
3057
3058 let expected_ns = vec![
3059 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
3060 NamespaceIdent::from_vec(vec!["ns2".to_string()]).unwrap(),
3061 NamespaceIdent::from_vec(vec!["ns3".to_string()]).unwrap(),
3062 NamespaceIdent::from_vec(vec!["ns4".to_string(), "ns41".to_string()]).unwrap(),
3063 NamespaceIdent::from_vec(vec![
3064 "ns5".to_string(),
3065 "ns51".to_string(),
3066 "ns511".to_string(),
3067 ])
3068 .unwrap(),
3069 NamespaceIdent::from_vec(vec!["ns6".to_string()]).unwrap(),
3070 NamespaceIdent::from_vec(vec!["ns7".to_string()]).unwrap(),
3071 NamespaceIdent::from_vec(vec!["ns8".to_string(), "ns81".to_string()]).unwrap(),
3072 ];
3073
3074 assert_eq!(expected_ns, namespaces);
3075
3076 config_mock.assert_async().await;
3078 list_ns_mock_page1.assert_async().await;
3079 list_ns_mock_page2.assert_async().await;
3080 list_ns_mock_page3.assert_async().await;
3081 list_ns_mock_page4.assert_async().await;
3082 list_ns_mock_page5.assert_async().await;
3083 }
3084
3085 #[tokio::test]
3086 async fn test_create_namespace() {
3087 let mut server = Server::new_async().await;
3088
3089 let config_mock = create_config_mock(&mut server).await;
3090
3091 let create_ns_mock = server
3092 .mock("POST", "/v1/namespaces")
3093 .with_body(
3094 r#"{
3095 "namespace": [ "ns1", "ns11"],
3096 "properties" : {
3097 "key1": "value1"
3098 }
3099 }"#,
3100 )
3101 .create_async()
3102 .await;
3103
3104 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3105
3106 let namespaces = catalog
3107 .create_namespace(
3108 &SessionContext::empty(),
3109 &NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
3110 HashMap::from([("key1".to_string(), "value1".to_string())]),
3111 )
3112 .await
3113 .unwrap();
3114
3115 let expected_ns = Namespace::with_properties(
3116 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
3117 HashMap::from([("key1".to_string(), "value1".to_string())]),
3118 );
3119
3120 assert_eq!(expected_ns, namespaces);
3121
3122 config_mock.assert_async().await;
3123 create_ns_mock.assert_async().await;
3124 }
3125
3126 #[tokio::test]
3127 async fn test_get_namespace() {
3128 let mut server = Server::new_async().await;
3129
3130 let config_mock = create_config_mock(&mut server).await;
3131
3132 let get_ns_mock = server
3133 .mock("GET", "/v1/namespaces/ns1")
3134 .with_body(
3135 r#"{
3136 "namespace": [ "ns1"],
3137 "properties" : {
3138 "key1": "value1"
3139 }
3140 }"#,
3141 )
3142 .create_async()
3143 .await;
3144
3145 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3146
3147 let namespaces = catalog
3148 .get_namespace(
3149 &SessionContext::empty(),
3150 &NamespaceIdent::new("ns1".to_string()),
3151 )
3152 .await
3153 .unwrap();
3154
3155 let expected_ns = Namespace::with_properties(
3156 NamespaceIdent::new("ns1".to_string()),
3157 HashMap::from([("key1".to_string(), "value1".to_string())]),
3158 );
3159
3160 assert_eq!(expected_ns, namespaces);
3161
3162 config_mock.assert_async().await;
3163 get_ns_mock.assert_async().await;
3164 }
3165
3166 #[tokio::test]
3167 async fn check_namespace_exists() {
3168 let mut server = Server::new_async().await;
3169
3170 let config_mock = create_config_mock_with_exists_endpoints(&mut server).await;
3171
3172 let get_ns_mock = server
3173 .mock("HEAD", "/v1/namespaces/ns1")
3174 .with_status(204)
3175 .create_async()
3176 .await;
3177
3178 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3179
3180 assert!(
3181 catalog
3182 .namespace_exists(
3183 &SessionContext::empty(),
3184 &NamespaceIdent::new("ns1".to_string())
3185 )
3186 .await
3187 .unwrap()
3188 );
3189
3190 config_mock.assert_async().await;
3191 get_ns_mock.assert_async().await;
3192 }
3193
3194 #[tokio::test]
3195 async fn test_namespace_exists_falls_back_to_get_when_head_not_advertised() {
3196 let mut server = Server::new_async().await;
3197
3198 let config_mock = create_config_mock(&mut server).await;
3201 let get_ns_mock = server
3202 .mock("GET", "/v1/namespaces/ns1")
3203 .with_status(200)
3204 .with_body(
3205 r#"{
3206 "namespace": ["ns1"],
3207 "properties": {}
3208 }"#,
3209 )
3210 .create_async()
3211 .await;
3212
3213 let catalog = RestCatalog::new(
3214 SessionContext::empty(),
3215 RestCatalogConfig::builder().uri(server.url()).build(),
3216 None,
3217 Some(Arc::new(LocalFsStorageFactory)),
3218 Runtime::current(),
3219 None,
3220 );
3221
3222 assert!(
3223 catalog
3224 .namespace_exists(&NamespaceIdent::new("ns1".to_string()))
3225 .await
3226 .unwrap()
3227 );
3228
3229 config_mock.assert_async().await;
3230 get_ns_mock.assert_async().await;
3231 }
3232
3233 #[tokio::test]
3234 async fn test_drop_namespace() {
3235 let mut server = Server::new_async().await;
3236
3237 let config_mock = create_config_mock(&mut server).await;
3238
3239 let drop_ns_mock = server
3240 .mock("DELETE", "/v1/namespaces/ns1")
3241 .with_status(204)
3242 .create_async()
3243 .await;
3244
3245 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3246
3247 catalog
3248 .drop_namespace(
3249 &SessionContext::empty(),
3250 &NamespaceIdent::new("ns1".to_string()),
3251 )
3252 .await
3253 .unwrap();
3254
3255 config_mock.assert_async().await;
3256 drop_ns_mock.assert_async().await;
3257 }
3258
3259 #[tokio::test]
3260 async fn test_list_tables() {
3261 let mut server = Server::new_async().await;
3262
3263 let config_mock = create_config_mock(&mut server).await;
3264
3265 let list_tables_mock = server
3266 .mock("GET", "/v1/namespaces/ns1/tables")
3267 .with_status(200)
3268 .with_body(
3269 r#"{
3270 "identifiers": [
3271 {
3272 "namespace": ["ns1"],
3273 "name": "table1"
3274 },
3275 {
3276 "namespace": ["ns1"],
3277 "name": "table2"
3278 }
3279 ]
3280 }"#,
3281 )
3282 .create_async()
3283 .await;
3284
3285 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3286
3287 let tables = catalog
3288 .list_tables(
3289 &SessionContext::empty(),
3290 &NamespaceIdent::new("ns1".to_string()),
3291 )
3292 .await
3293 .unwrap();
3294
3295 let expected_tables = vec![
3296 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
3297 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
3298 ];
3299
3300 assert_eq!(tables, expected_tables);
3301
3302 config_mock.assert_async().await;
3303 list_tables_mock.assert_async().await;
3304 }
3305
3306 #[tokio::test]
3307 async fn test_list_tables_with_pagination() {
3308 let mut server = Server::new_async().await;
3309
3310 let config_mock = create_config_mock(&mut server).await;
3311
3312 let list_tables_mock_page1 = server
3313 .mock("GET", "/v1/namespaces/ns1/tables")
3314 .with_status(200)
3315 .with_body(
3316 r#"{
3317 "identifiers": [
3318 {
3319 "namespace": ["ns1"],
3320 "name": "table1"
3321 },
3322 {
3323 "namespace": ["ns1"],
3324 "name": "table2"
3325 }
3326 ],
3327 "next-page-token": "token456"
3328 }"#,
3329 )
3330 .create_async()
3331 .await;
3332
3333 let list_tables_mock_page2 = server
3334 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=token456")
3335 .with_status(200)
3336 .with_body(
3337 r#"{
3338 "identifiers": [
3339 {
3340 "namespace": ["ns1"],
3341 "name": "table3"
3342 },
3343 {
3344 "namespace": ["ns1"],
3345 "name": "table4"
3346 }
3347 ]
3348 }"#,
3349 )
3350 .create_async()
3351 .await;
3352
3353 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3354
3355 let tables = catalog
3356 .list_tables(
3357 &SessionContext::empty(),
3358 &NamespaceIdent::new("ns1".to_string()),
3359 )
3360 .await
3361 .unwrap();
3362
3363 let expected_tables = vec![
3364 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
3365 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
3366 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table3".to_string()),
3367 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table4".to_string()),
3368 ];
3369
3370 assert_eq!(tables, expected_tables);
3371
3372 config_mock.assert_async().await;
3373 list_tables_mock_page1.assert_async().await;
3374 list_tables_mock_page2.assert_async().await;
3375 }
3376
3377 #[tokio::test]
3378 async fn test_list_tables_with_multiple_pages() {
3379 let mut server = Server::new_async().await;
3380
3381 let config_mock = create_config_mock(&mut server).await;
3382
3383 let list_tables_mock_page1 = server
3385 .mock("GET", "/v1/namespaces/ns1/tables")
3386 .with_status(200)
3387 .with_body(
3388 r#"{
3389 "identifiers": [
3390 {
3391 "namespace": ["ns1"],
3392 "name": "table1"
3393 },
3394 {
3395 "namespace": ["ns1"],
3396 "name": "table2"
3397 }
3398 ],
3399 "next-page-token": "page2"
3400 }"#,
3401 )
3402 .create_async()
3403 .await;
3404
3405 let list_tables_mock_page2 = server
3407 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page2")
3408 .with_status(200)
3409 .with_body(
3410 r#"{
3411 "identifiers": [
3412 {
3413 "namespace": ["ns1"],
3414 "name": "table3"
3415 },
3416 {
3417 "namespace": ["ns1"],
3418 "name": "table4"
3419 }
3420 ],
3421 "next-page-token": "page3"
3422 }"#,
3423 )
3424 .create_async()
3425 .await;
3426
3427 let list_tables_mock_page3 = server
3429 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page3")
3430 .with_status(200)
3431 .with_body(
3432 r#"{
3433 "identifiers": [
3434 {
3435 "namespace": ["ns1"],
3436 "name": "table5"
3437 }
3438 ],
3439 "next-page-token": "page4"
3440 }"#,
3441 )
3442 .create_async()
3443 .await;
3444
3445 let list_tables_mock_page4 = server
3447 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page4")
3448 .with_status(200)
3449 .with_body(
3450 r#"{
3451 "identifiers": [
3452 {
3453 "namespace": ["ns1"],
3454 "name": "table6"
3455 },
3456 {
3457 "namespace": ["ns1"],
3458 "name": "table7"
3459 }
3460 ],
3461 "next-page-token": "page5"
3462 }"#,
3463 )
3464 .create_async()
3465 .await;
3466
3467 let list_tables_mock_page5 = server
3469 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page5")
3470 .with_status(200)
3471 .with_body(
3472 r#"{
3473 "identifiers": [
3474 {
3475 "namespace": ["ns1"],
3476 "name": "table8"
3477 }
3478 ]
3479 }"#,
3480 )
3481 .create_async()
3482 .await;
3483
3484 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3485
3486 let tables = catalog
3487 .list_tables(
3488 &SessionContext::empty(),
3489 &NamespaceIdent::new("ns1".to_string()),
3490 )
3491 .await
3492 .unwrap();
3493
3494 let expected_tables = vec![
3495 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
3496 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
3497 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table3".to_string()),
3498 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table4".to_string()),
3499 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table5".to_string()),
3500 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table6".to_string()),
3501 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table7".to_string()),
3502 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table8".to_string()),
3503 ];
3504
3505 assert_eq!(tables, expected_tables);
3506
3507 config_mock.assert_async().await;
3509 list_tables_mock_page1.assert_async().await;
3510 list_tables_mock_page2.assert_async().await;
3511 list_tables_mock_page3.assert_async().await;
3512 list_tables_mock_page4.assert_async().await;
3513 list_tables_mock_page5.assert_async().await;
3514 }
3515
3516 #[tokio::test]
3517 async fn test_drop_tables() {
3518 let mut server = Server::new_async().await;
3519
3520 let config_mock = create_config_mock(&mut server).await;
3521
3522 let delete_table_mock = server
3523 .mock("DELETE", "/v1/namespaces/ns1/tables/table1")
3524 .with_status(204)
3525 .create_async()
3526 .await;
3527
3528 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3529
3530 catalog
3531 .drop_table(
3532 &SessionContext::empty(),
3533 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
3534 )
3535 .await
3536 .unwrap();
3537
3538 config_mock.assert_async().await;
3539 delete_table_mock.assert_async().await;
3540 }
3541
3542 #[tokio::test]
3543 async fn test_check_table_exists() {
3544 let mut server = Server::new_async().await;
3545
3546 let config_mock = create_config_mock_with_exists_endpoints(&mut server).await;
3547
3548 let check_table_exists_mock = server
3549 .mock("HEAD", "/v1/namespaces/ns1/tables/table1")
3550 .with_status(204)
3551 .create_async()
3552 .await;
3553
3554 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3555
3556 assert!(
3557 catalog
3558 .table_exists(
3559 &SessionContext::empty(),
3560 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string(),),
3561 )
3562 .await
3563 .unwrap()
3564 );
3565
3566 config_mock.assert_async().await;
3567 check_table_exists_mock.assert_async().await;
3568 }
3569
3570 #[tokio::test]
3571 async fn test_table_exists_falls_back_to_load_when_head_not_advertised() {
3572 let mut server = Server::new_async().await;
3573
3574 let config_mock = create_config_mock(&mut server).await;
3577 let load_table_mock = server
3578 .mock("GET", "/v1/namespaces/ns1/tables/table1")
3579 .with_status(200)
3580 .with_body_from_file(format!(
3581 "{}/testdata/{}",
3582 env!("CARGO_MANIFEST_DIR"),
3583 "load_table_response.json"
3584 ))
3585 .create_async()
3586 .await;
3587
3588 let catalog = RestCatalog::new(
3589 SessionContext::empty(),
3590 RestCatalogConfig::builder().uri(server.url()).build(),
3591 None,
3592 Some(Arc::new(LocalFsStorageFactory)),
3593 Runtime::current(),
3594 None,
3595 );
3596
3597 assert!(
3598 catalog
3599 .table_exists(&TableIdent::new(
3600 NamespaceIdent::new("ns1".to_string()),
3601 "table1".to_string(),
3602 ))
3603 .await
3604 .unwrap()
3605 );
3606
3607 config_mock.assert_async().await;
3608 load_table_mock.assert_async().await;
3609 }
3610
3611 #[tokio::test]
3612 async fn test_rename_table() {
3613 let mut server = Server::new_async().await;
3614
3615 let config_mock = create_config_mock(&mut server).await;
3616
3617 let rename_table_mock = server
3618 .mock("POST", "/v1/tables/rename")
3619 .with_status(204)
3620 .create_async()
3621 .await;
3622
3623 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3624
3625 catalog
3626 .rename_table(
3627 &SessionContext::empty(),
3628 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
3629 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
3630 )
3631 .await
3632 .unwrap();
3633
3634 config_mock.assert_async().await;
3635 rename_table_mock.assert_async().await;
3636 }
3637
3638 #[tokio::test]
3639 async fn test_load_table() {
3640 let mut server = Server::new_async().await;
3641
3642 let config_mock = create_config_mock(&mut server).await;
3643
3644 let rename_table_mock = server
3645 .mock("GET", "/v1/namespaces/ns1/tables/test1")
3646 .with_status(200)
3647 .with_body_from_file(format!(
3648 "{}/testdata/{}",
3649 env!("CARGO_MANIFEST_DIR"),
3650 "load_table_response.json"
3651 ))
3652 .create_async()
3653 .await;
3654
3655 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3656
3657 let table = catalog
3658 .load_table(
3659 &SessionContext::empty(),
3660 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "test1".to_string()),
3661 )
3662 .await
3663 .unwrap();
3664
3665 assert_eq!(
3666 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
3667 table.identifier()
3668 );
3669 assert_eq!(
3670 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
3671 table.metadata_location().unwrap()
3672 );
3673 assert_eq!(FormatVersion::V1, table.metadata().format_version());
3674 assert_eq!("s3://warehouse/database/table", table.metadata().location());
3675 assert_eq!(
3676 uuid!("b55d9dda-6561-423a-8bfc-787980ce421f"),
3677 table.metadata().uuid()
3678 );
3679 assert_eq!(
3680 Utc.timestamp_millis_opt(1646787054459).unwrap(),
3681 table.metadata().last_updated_timestamp().unwrap()
3682 );
3683 assert_eq!(
3684 vec![&Arc::new(
3685 Schema::builder()
3686 .with_fields(vec![
3687 NestedField::optional(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
3688 NestedField::optional(2, "data", Type::Primitive(PrimitiveType::String))
3689 .into(),
3690 ])
3691 .build()
3692 .unwrap()
3693 )],
3694 table.metadata().schemas_iter().collect::<Vec<_>>()
3695 );
3696 assert_eq!(
3697 &HashMap::from([
3698 ("owner".to_string(), "bryan".to_string()),
3699 (
3700 "write.metadata.compression-codec".to_string(),
3701 "gzip".to_string()
3702 )
3703 ]),
3704 table.metadata().properties()
3705 );
3706 assert_eq!(vec![&Arc::new(Snapshot::builder()
3707 .with_snapshot_id(3497810964824022504)
3708 .with_timestamp_ms(1646787054459)
3709 .with_manifest_list("s3://warehouse/database/table/metadata/snap-3497810964824022504-1-c4f68204-666b-4e50-a9df-b10c34bf6b82.avro")
3710 .with_sequence_number(0)
3711 .with_schema_id(0)
3712 .with_summary(Summary {
3713 operation: Operation::Append,
3714 additional_properties: HashMap::from_iter([
3715 ("spark.app.id", "local-1646787004168"),
3716 ("added-data-files", "1"),
3717 ("added-records", "1"),
3718 ("added-files-size", "697"),
3719 ("changed-partition-count", "1"),
3720 ("total-records", "1"),
3721 ("total-files-size", "697"),
3722 ("total-data-files", "1"),
3723 ("total-delete-files", "0"),
3724 ("total-position-deletes", "0"),
3725 ("total-equality-deletes", "0")
3726 ].iter().map(|p| (p.0.to_string(), p.1.to_string()))),
3727 }).build()
3728 )], table.metadata().snapshots().collect::<Vec<_>>());
3729 assert_eq!(
3730 &[SnapshotLog {
3731 timestamp_ms: 1646787054459,
3732 snapshot_id: 3497810964824022504,
3733 }],
3734 table.metadata().history()
3735 );
3736 assert_eq!(
3737 vec![&Arc::new(SortOrder {
3738 order_id: 0,
3739 fields: vec![],
3740 })],
3741 table.metadata().sort_orders_iter().collect::<Vec<_>>()
3742 );
3743
3744 config_mock.assert_async().await;
3745 rename_table_mock.assert_async().await;
3746 }
3747
3748 #[tokio::test]
3749 async fn test_load_table_404() {
3750 let mut server = Server::new_async().await;
3751
3752 let config_mock = create_config_mock(&mut server).await;
3753
3754 let rename_table_mock = server
3755 .mock("GET", "/v1/namespaces/ns1/tables/test1")
3756 .with_status(404)
3757 .with_body(r#"
3758{
3759 "error": {
3760 "message": "Table does not exist: ns1.test1 in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
3761 "type": "NoSuchNamespaceErrorException",
3762 "code": 404
3763 }
3764}
3765 "#)
3766 .create_async()
3767 .await;
3768
3769 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3770
3771 let table = catalog
3772 .load_table(
3773 &SessionContext::empty(),
3774 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "test1".to_string()),
3775 )
3776 .await;
3777
3778 assert!(table.is_err());
3779 assert!(table.err().unwrap().message().contains("does not exist"));
3780
3781 config_mock.assert_async().await;
3782 rename_table_mock.assert_async().await;
3783 }
3784
3785 #[tokio::test]
3786 async fn test_create_table() {
3787 let mut server = Server::new_async().await;
3788
3789 let config_mock = create_config_mock(&mut server).await;
3790
3791 let create_table_mock = server
3792 .mock("POST", "/v1/namespaces/ns1/tables")
3793 .with_status(200)
3794 .with_body_from_file(format!(
3795 "{}/testdata/{}",
3796 env!("CARGO_MANIFEST_DIR"),
3797 "create_table_response.json"
3798 ))
3799 .create_async()
3800 .await;
3801
3802 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3803
3804 let table_creation = TableCreation::builder()
3805 .name("test1".to_string())
3806 .schema(
3807 Schema::builder()
3808 .with_fields(vec![
3809 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
3810 .into(),
3811 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
3812 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
3813 .into(),
3814 ])
3815 .with_schema_id(1)
3816 .with_identifier_field_ids(vec![2])
3817 .build()
3818 .unwrap(),
3819 )
3820 .properties(HashMap::from([("owner".to_string(), "testx".to_string())]))
3821 .partition_spec(
3822 UnboundPartitionSpec::builder()
3823 .add_partition_fields(vec![
3824 UnboundPartitionField::builder()
3825 .source_id(1)
3826 .transform(Transform::Truncate(3))
3827 .name("id".to_string())
3828 .build(),
3829 ])
3830 .unwrap()
3831 .build(),
3832 )
3833 .sort_order(
3834 SortOrder::builder()
3835 .with_sort_field(
3836 SortField::builder()
3837 .source_id(2)
3838 .transform(Transform::Identity)
3839 .direction(SortDirection::Ascending)
3840 .null_order(NullOrder::First)
3841 .build(),
3842 )
3843 .build_unbound()
3844 .unwrap(),
3845 )
3846 .build();
3847
3848 let table = catalog
3849 .create_table(
3850 &SessionContext::empty(),
3851 &NamespaceIdent::from_strs(["ns1"]).unwrap(),
3852 table_creation,
3853 )
3854 .await
3855 .unwrap();
3856
3857 assert_eq!(
3858 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
3859 table.identifier()
3860 );
3861 assert_eq!(
3862 "s3://warehouse/database/table/metadata.json",
3863 table.metadata_location().unwrap()
3864 );
3865 assert_eq!(FormatVersion::V1, table.metadata().format_version());
3866 assert_eq!("s3://warehouse/database/table", table.metadata().location());
3867 assert_eq!(
3868 uuid!("bf289591-dcc0-4234-ad4f-5c3eed811a29"),
3869 table.metadata().uuid()
3870 );
3871 assert_eq!(
3872 1657810967051,
3873 table
3874 .metadata()
3875 .last_updated_timestamp()
3876 .unwrap()
3877 .timestamp_millis()
3878 );
3879 assert_eq!(
3880 vec![&Arc::new(
3881 Schema::builder()
3882 .with_fields(vec![
3883 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
3884 .into(),
3885 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
3886 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
3887 .into(),
3888 ])
3889 .with_schema_id(0)
3890 .with_identifier_field_ids(vec![2])
3891 .build()
3892 .unwrap()
3893 )],
3894 table.metadata().schemas_iter().collect::<Vec<_>>()
3895 );
3896 assert_eq!(
3897 &HashMap::from([
3898 (
3899 "write.delete.parquet.compression-codec".to_string(),
3900 "zstd".to_string()
3901 ),
3902 (
3903 "write.metadata.compression-codec".to_string(),
3904 "gzip".to_string()
3905 ),
3906 (
3907 "write.summary.partition-limit".to_string(),
3908 "100".to_string()
3909 ),
3910 (
3911 "write.parquet.compression-codec".to_string(),
3912 "zstd".to_string()
3913 ),
3914 ]),
3915 table.metadata().properties()
3916 );
3917 assert!(table.metadata().current_snapshot().is_none());
3918 assert!(table.metadata().history().is_empty());
3919 assert_eq!(
3920 vec![&Arc::new(SortOrder {
3921 order_id: 0,
3922 fields: vec![],
3923 })],
3924 table.metadata().sort_orders_iter().collect::<Vec<_>>()
3925 );
3926
3927 config_mock.assert_async().await;
3928 create_table_mock.assert_async().await;
3929 }
3930
3931 #[tokio::test]
3932 async fn test_create_table_409() {
3933 let mut server = Server::new_async().await;
3934
3935 let config_mock = create_config_mock(&mut server).await;
3936
3937 let create_table_mock = server
3938 .mock("POST", "/v1/namespaces/ns1/tables")
3939 .with_status(409)
3940 .with_body(r#"
3941{
3942 "error": {
3943 "message": "Table already exists: ns1.test1 in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
3944 "type": "AlreadyExistsException",
3945 "code": 409
3946 }
3947}
3948 "#)
3949 .create_async()
3950 .await;
3951
3952 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
3953
3954 let table_creation = TableCreation::builder()
3955 .name("test1".to_string())
3956 .schema(
3957 Schema::builder()
3958 .with_fields(vec![
3959 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
3960 .into(),
3961 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
3962 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
3963 .into(),
3964 ])
3965 .with_schema_id(1)
3966 .with_identifier_field_ids(vec![2])
3967 .build()
3968 .unwrap(),
3969 )
3970 .properties(HashMap::from([("owner".to_string(), "testx".to_string())]))
3971 .build();
3972
3973 let table_result = catalog
3974 .create_table(
3975 &SessionContext::empty(),
3976 &NamespaceIdent::from_strs(["ns1"]).unwrap(),
3977 table_creation,
3978 )
3979 .await;
3980
3981 assert!(table_result.is_err());
3982 assert!(
3983 table_result
3984 .err()
3985 .unwrap()
3986 .message()
3987 .contains("already exists")
3988 );
3989
3990 config_mock.assert_async().await;
3991 create_table_mock.assert_async().await;
3992 }
3993
3994 #[tokio::test]
3995 async fn test_update_table() {
3996 let mut server = Server::new_async().await;
3997
3998 let config_mock = create_config_mock(&mut server).await;
3999
4000 let load_table_mock = server
4001 .mock("GET", "/v1/namespaces/ns1/tables/test1")
4002 .with_status(200)
4003 .with_body_from_file(format!(
4004 "{}/testdata/{}",
4005 env!("CARGO_MANIFEST_DIR"),
4006 "load_table_response.json"
4007 ))
4008 .create_async()
4009 .await;
4010
4011 let update_table_mock = server
4012 .mock("POST", "/v1/namespaces/ns1/tables/test1")
4013 .with_status(200)
4014 .with_body_from_file(format!(
4015 "{}/testdata/{}",
4016 env!("CARGO_MANIFEST_DIR"),
4017 "update_table_response.json"
4018 ))
4019 .create_async()
4020 .await;
4021
4022 let catalog = RestCatalog::new(
4023 SessionContext::empty(),
4024 RestCatalogConfig::builder().uri(server.url()).build(),
4025 None,
4026 Some(Arc::new(LocalFsStorageFactory)),
4027 Runtime::current(),
4028 None,
4029 );
4030
4031 let table1 = {
4032 let file = File::open(format!(
4033 "{}/testdata/{}",
4034 env!("CARGO_MANIFEST_DIR"),
4035 "create_table_response.json"
4036 ))
4037 .unwrap();
4038 let reader = BufReader::new(file);
4039 let resp = serde_json::from_reader::<_, LoadTableResult>(reader).unwrap();
4040
4041 Table::builder()
4042 .metadata(resp.metadata)
4043 .metadata_location(resp.metadata_location.unwrap())
4044 .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
4045 .file_io(FileIO::new_with_fs())
4046 .runtime(test_runtime())
4047 .build()
4048 .unwrap()
4049 };
4050
4051 let tx = Transaction::new(&table1);
4052 let table = tx
4053 .upgrade_table_version()
4054 .set_format_version(FormatVersion::V2)
4055 .apply(tx)
4056 .unwrap()
4057 .commit(&catalog)
4058 .await
4059 .unwrap();
4060
4061 assert_eq!(
4062 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
4063 table.identifier()
4064 );
4065 assert_eq!(
4066 "s3://warehouse/database/table/metadata.json",
4067 table.metadata_location().unwrap()
4068 );
4069 assert_eq!(FormatVersion::V2, table.metadata().format_version());
4070 assert_eq!("s3://warehouse/database/table", table.metadata().location());
4071 assert_eq!(
4072 uuid!("bf289591-dcc0-4234-ad4f-5c3eed811a29"),
4073 table.metadata().uuid()
4074 );
4075 assert_eq!(
4076 1657810967051,
4077 table
4078 .metadata()
4079 .last_updated_timestamp()
4080 .unwrap()
4081 .timestamp_millis()
4082 );
4083 assert_eq!(
4084 vec![&Arc::new(
4085 Schema::builder()
4086 .with_fields(vec![
4087 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
4088 .into(),
4089 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
4090 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
4091 .into(),
4092 ])
4093 .with_schema_id(0)
4094 .with_identifier_field_ids(vec![2])
4095 .build()
4096 .unwrap()
4097 )],
4098 table.metadata().schemas_iter().collect::<Vec<_>>()
4099 );
4100 assert_eq!(
4101 &HashMap::from([
4102 (
4103 "write.delete.parquet.compression-codec".to_string(),
4104 "zstd".to_string()
4105 ),
4106 (
4107 "write.metadata.compression-codec".to_string(),
4108 "gzip".to_string()
4109 ),
4110 (
4111 "write.summary.partition-limit".to_string(),
4112 "100".to_string()
4113 ),
4114 (
4115 "write.parquet.compression-codec".to_string(),
4116 "zstd".to_string()
4117 ),
4118 ]),
4119 table.metadata().properties()
4120 );
4121 assert!(table.metadata().current_snapshot().is_none());
4122 assert!(table.metadata().history().is_empty());
4123 assert_eq!(
4124 vec![&Arc::new(SortOrder {
4125 order_id: 0,
4126 fields: vec![],
4127 })],
4128 table.metadata().sort_orders_iter().collect::<Vec<_>>()
4129 );
4130
4131 config_mock.assert_async().await;
4132 update_table_mock.assert_async().await;
4133 load_table_mock.assert_async().await
4134 }
4135
4136 #[tokio::test]
4137 async fn test_update_table_404() {
4138 let mut server = Server::new_async().await;
4139
4140 let config_mock = create_config_mock(&mut server).await;
4141
4142 let load_table_mock = server
4143 .mock("GET", "/v1/namespaces/ns1/tables/test1")
4144 .with_status(200)
4145 .with_body_from_file(format!(
4146 "{}/testdata/{}",
4147 env!("CARGO_MANIFEST_DIR"),
4148 "load_table_response.json"
4149 ))
4150 .create_async()
4151 .await;
4152
4153 let update_table_mock = server
4154 .mock("POST", "/v1/namespaces/ns1/tables/test1")
4155 .with_status(404)
4156 .with_body(
4157 r#"
4158{
4159 "error": {
4160 "message": "The given table does not exist",
4161 "type": "NoSuchTableException",
4162 "code": 404
4163 }
4164}
4165 "#,
4166 )
4167 .create_async()
4168 .await;
4169
4170 let catalog = RestCatalog::new(
4171 SessionContext::empty(),
4172 RestCatalogConfig::builder().uri(server.url()).build(),
4173 None,
4174 Some(Arc::new(LocalFsStorageFactory)),
4175 Runtime::current(),
4176 None,
4177 );
4178
4179 let table1 = {
4180 let file = File::open(format!(
4181 "{}/testdata/{}",
4182 env!("CARGO_MANIFEST_DIR"),
4183 "create_table_response.json"
4184 ))
4185 .unwrap();
4186 let reader = BufReader::new(file);
4187 let resp = serde_json::from_reader::<_, LoadTableResult>(reader).unwrap();
4188
4189 Table::builder()
4190 .metadata(resp.metadata)
4191 .metadata_location(resp.metadata_location.unwrap())
4192 .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
4193 .file_io(FileIO::new_with_fs())
4194 .runtime(test_runtime())
4195 .build()
4196 .unwrap()
4197 };
4198
4199 let tx = Transaction::new(&table1);
4200 let table_result = tx
4201 .upgrade_table_version()
4202 .set_format_version(FormatVersion::V2)
4203 .apply(tx)
4204 .unwrap()
4205 .commit(&catalog)
4206 .await;
4207
4208 assert!(table_result.is_err());
4209 assert!(
4210 table_result
4211 .err()
4212 .unwrap()
4213 .message()
4214 .contains("does not exist")
4215 );
4216
4217 config_mock.assert_async().await;
4218 update_table_mock.assert_async().await;
4219 load_table_mock.assert_async().await;
4220 }
4221
4222 #[tokio::test]
4223 async fn test_register_table() {
4224 let mut server = Server::new_async().await;
4225
4226 let config_mock = create_config_mock(&mut server).await;
4227
4228 let register_table_mock = server
4229 .mock("POST", "/v1/namespaces/ns1/register")
4230 .with_status(200)
4231 .with_body_from_file(format!(
4232 "{}/testdata/{}",
4233 env!("CARGO_MANIFEST_DIR"),
4234 "load_table_response.json"
4235 ))
4236 .create_async()
4237 .await;
4238
4239 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
4240 let table_ident =
4241 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "test1".to_string());
4242 let metadata_location = String::from(
4243 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
4244 );
4245
4246 let table = catalog
4247 .register_table(&SessionContext::empty(), &table_ident, metadata_location)
4248 .await
4249 .unwrap();
4250
4251 assert_eq!(
4252 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
4253 table.identifier()
4254 );
4255 assert_eq!(
4256 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
4257 table.metadata_location().unwrap()
4258 );
4259
4260 config_mock.assert_async().await;
4261 register_table_mock.assert_async().await;
4262 }
4263
4264 #[tokio::test]
4265 async fn test_register_table_404() {
4266 let mut server = Server::new_async().await;
4267
4268 let config_mock = create_config_mock(&mut server).await;
4269
4270 let register_table_mock = server
4271 .mock("POST", "/v1/namespaces/ns1/register")
4272 .with_status(404)
4273 .with_body(
4274 r#"
4275{
4276 "error": {
4277 "message": "The namespace specified does not exist",
4278 "type": "NoSuchNamespaceErrorException",
4279 "code": 404
4280 }
4281}
4282 "#,
4283 )
4284 .create_async()
4285 .await;
4286
4287 let catalog = session_catalog(RestCatalogConfig::builder().uri(server.url()).build());
4288
4289 let table_ident =
4290 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "test1".to_string());
4291 let metadata_location = String::from(
4292 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
4293 );
4294 let table = catalog
4295 .register_table(&SessionContext::empty(), &table_ident, metadata_location)
4296 .await;
4297
4298 assert!(table.is_err());
4299 assert!(table.err().unwrap().message().contains("does not exist"));
4300
4301 config_mock.assert_async().await;
4302 register_table_mock.assert_async().await;
4303 }
4304
4305 #[tokio::test]
4306 async fn test_create_rest_catalog() {
4307 let builder = RestCatalogBuilder::default().with_client(Client::new());
4308
4309 let catalog = builder
4310 .load(
4311 "test",
4312 HashMap::from([
4313 (
4314 REST_CATALOG_PROP_URI.to_string(),
4315 "http://localhost:8080".to_string(),
4316 ),
4317 ("a".to_string(), "b".to_string()),
4318 ]),
4319 )
4320 .await;
4321
4322 assert!(catalog.is_ok());
4323
4324 let catalog = catalog.unwrap();
4325 let catalog_config = &catalog.inner.user_config;
4326 assert_eq!(catalog_config.name.as_deref(), Some("test"));
4327 assert_eq!(catalog_config.uri, "http://localhost:8080");
4328 assert_eq!(catalog_config.warehouse, None);
4329 assert!(catalog_config.client.is_some());
4330
4331 assert_eq!(catalog_config.props.get("a"), Some(&"b".to_string()));
4332 assert!(!catalog_config.props.contains_key(REST_CATALOG_PROP_URI));
4333 }
4334
4335 #[tokio::test]
4336 async fn test_create_rest_catalog_no_uri() {
4337 let builder = RestCatalogBuilder::default();
4338
4339 let catalog = builder
4340 .load(
4341 "test",
4342 HashMap::from([(
4343 REST_CATALOG_PROP_WAREHOUSE.to_string(),
4344 "s3://warehouse".to_string(),
4345 )]),
4346 )
4347 .await;
4348
4349 assert!(catalog.is_err());
4350 if let Err(err) = catalog {
4351 assert_eq!(err.kind(), ErrorKind::DataInvalid);
4352 assert_eq!(err.message(), "Catalog uri is required");
4353 }
4354 }
4355
4356 #[tokio::test]
4357 async fn test_create_session_catalog() {
4358 let builder = RestSessionCatalogBuilder::default();
4359
4360 let result = builder
4361 .load(
4362 "test",
4363 HashMap::from([
4364 (
4365 REST_CATALOG_PROP_URI.to_string(),
4366 "http://localhost:8080".to_string(),
4367 ),
4368 ("a".to_string(), "b".to_string()),
4369 ]),
4370 )
4371 .await;
4372
4373 assert!(result.is_ok());
4374
4375 let catalog = result.unwrap();
4376
4377 let catalog_config = catalog.user_config;
4378 assert_eq!(catalog_config.name.as_deref(), Some("test"));
4379 assert_eq!(catalog_config.uri, "http://localhost:8080");
4380 assert_eq!(catalog_config.warehouse, None);
4381 assert!(catalog_config.client.is_none());
4383
4384 assert_eq!(catalog_config.props.get("a"), Some(&"b".to_string()));
4386 assert!(!catalog_config.props.contains_key(REST_CATALOG_PROP_URI));
4387 }
4388
4389 #[tokio::test]
4390 async fn test_create_rest_catalog_with_session() {
4391 let context = SessionContext::builder()
4392 .session_id("test-id".to_string())
4393 .build();
4394
4395 let result = RestCatalogBuilder::default()
4396 .with_session_context(context)
4397 .load(
4398 "test",
4399 HashMap::from([(
4400 REST_CATALOG_PROP_URI.to_string(),
4401 "http://localhost:8080".to_string(),
4402 )]),
4403 )
4404 .await;
4405
4406 assert!(result.is_ok());
4407
4408 let catalog = result.unwrap();
4410 assert_eq!(catalog.session_context.session_id(), "test-id");
4411 }
4412
4413 #[tokio::test]
4414 async fn test_create_rest_catalog_default_session() {
4415 let result = RestCatalogBuilder::default()
4416 .load(
4417 "test",
4418 HashMap::from([(
4419 REST_CATALOG_PROP_URI.to_string(),
4420 "http://localhost:8080".to_string(),
4421 )]),
4422 )
4423 .await;
4424
4425 assert!(result.is_ok());
4426
4427 let catalog = result.unwrap();
4430 assert!(uuid::Uuid::parse_str(catalog.session_context.session_id()).is_ok());
4431 }
4432
4433 #[tokio::test]
4435 async fn test_rest_catalog_delegates() {
4436 let mut server = Server::new_async().await;
4437 let config_mock = create_config_mock(&mut server).await;
4438 let list_ns_mock = server
4439 .mock("GET", "/v1/namespaces")
4440 .with_body(
4441 r#"{
4442 "namespaces": [["ns1"]]
4443 }"#,
4444 )
4445 .create_async()
4446 .await;
4447
4448 let catalog = RestCatalog::new(
4449 SessionContext::empty(),
4450 RestCatalogConfig::builder().uri(server.url()).build(),
4451 None,
4452 Some(Arc::new(LocalFsStorageFactory)),
4453 Runtime::current(),
4454 None,
4455 );
4456
4457 let namespaces = catalog.list_namespaces(None).await.unwrap();
4458
4459 assert_eq!(namespaces, vec![
4460 NamespaceIdent::from_vec(vec!["ns1".to_string()]).unwrap()
4461 ]);
4462
4463 config_mock.assert_async().await;
4464 list_ns_mock.assert_async().await;
4465 }
4466}