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