1use std::collections::{HashMap, HashSet};
21use std::future::Future;
22use std::str::FromStr;
23use std::sync::Arc;
24
25use async_trait::async_trait;
26use iceberg::encryption::kms::{KeyManagementClient, KmsClientFactory};
27use iceberg::io::{FileIO, FileIOBuilder, StorageFactory};
28use iceberg::table::Table;
29use iceberg::{
30 Catalog, CatalogBuilder, Error, ErrorKind, Namespace, NamespaceIdent, Result, Runtime,
31 TableCommit, TableCreation, TableIdent,
32};
33use itertools::Itertools;
34use reqwest::header::{
35 HeaderMap, HeaderName, HeaderValue, {self},
36};
37use reqwest::{Client, Method, StatusCode, Url};
38use tokio::sync::OnceCell;
39use typed_builder::TypedBuilder;
40
41use crate::client::{
42 HttpClient, deserialize_catalog_response, deserialize_unexpected_catalog_error,
43};
44use crate::endpoint::{Endpoint, V1_NAMESPACE_EXISTS, V1_TABLE_EXISTS};
45use crate::types::{
46 CatalogConfig, CommitTableRequest, CommitTableResponse, CreateNamespaceRequest,
47 CreateTableRequest, ListNamespaceResponse, ListTablesResponse, LoadTableResult,
48 NamespaceResponse, RegisterTableRequest, RenameTableRequest,
49};
50
51pub const REST_CATALOG_PROP_URI: &str = "uri";
53pub const REST_CATALOG_PROP_WAREHOUSE: &str = "warehouse";
55pub const REST_CATALOG_PROP_DISABLE_HEADER_REDACTION: &str = "disable-header-redaction";
57
58const ICEBERG_REST_SPEC_VERSION: &str = "0.14.1";
59const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
60const PATH_V1: &str = "v1";
61
62#[derive(Debug)]
64pub struct RestCatalogBuilder {
65 config: RestCatalogConfig,
66 storage_factory: Option<Arc<dyn StorageFactory>>,
67 kms_client_factory: Option<Arc<dyn KmsClientFactory>>,
68 runtime: Option<Runtime>,
69}
70
71impl Default for RestCatalogBuilder {
72 fn default() -> Self {
73 Self {
74 config: RestCatalogConfig {
75 name: None,
76 uri: "".to_string(),
77 warehouse: None,
78 props: HashMap::new(),
79 client: None,
80 },
81 storage_factory: None,
82 kms_client_factory: None,
83 runtime: None,
84 }
85 }
86}
87
88impl CatalogBuilder for RestCatalogBuilder {
89 type C = RestCatalog;
90
91 fn with_storage_factory(mut self, storage_factory: Arc<dyn StorageFactory>) -> Self {
92 self.storage_factory = Some(storage_factory);
93 self
94 }
95
96 fn with_kms_client_factory(mut self, kms_client_factory: Arc<dyn KmsClientFactory>) -> Self {
97 self.kms_client_factory = Some(kms_client_factory);
98 self
99 }
100
101 fn with_runtime(mut self, runtime: Runtime) -> Self {
102 self.runtime = Some(runtime);
103 self
104 }
105
106 fn load(
107 mut self,
108 name: impl Into<String>,
109 props: HashMap<String, String>,
110 ) -> impl Future<Output = Result<Self::C>> + Send {
111 self.config.name = Some(name.into());
112
113 if props.contains_key(REST_CATALOG_PROP_URI) {
114 self.config.uri = props
115 .get(REST_CATALOG_PROP_URI)
116 .cloned()
117 .unwrap_or_default();
118 }
119
120 if props.contains_key(REST_CATALOG_PROP_WAREHOUSE) {
121 self.config.warehouse = props.get(REST_CATALOG_PROP_WAREHOUSE).cloned()
122 }
123
124 self.config.props = props
126 .into_iter()
127 .filter(|(k, _)| k != REST_CATALOG_PROP_URI && k != REST_CATALOG_PROP_WAREHOUSE)
128 .collect();
129
130 async move {
131 if self.config.name.is_none() {
132 Err(Error::new(
133 ErrorKind::DataInvalid,
134 "Catalog name is required",
135 ))
136 } else if self.config.uri.is_empty() {
137 Err(Error::new(
138 ErrorKind::DataInvalid,
139 "Catalog uri is required",
140 ))
141 } else {
142 let runtime = self.runtime.unwrap_or_else(Runtime::current);
143 let kms_client = match self.kms_client_factory {
144 Some(factory) => Some(factory.create_kms_client(&self.config.props).await?),
145 None => None,
146 };
147 Ok(RestCatalog::new(
148 self.config,
149 self.storage_factory,
150 runtime,
151 kms_client,
152 ))
153 }
154 }
155 }
156}
157
158impl RestCatalogBuilder {
159 pub fn with_client(mut self, client: Client) -> Self {
161 self.config.client = Some(client);
162 self
163 }
164}
165
166#[derive(Clone, Debug, TypedBuilder)]
168pub(crate) struct RestCatalogConfig {
169 #[builder(default, setter(strip_option))]
170 name: Option<String>,
171
172 uri: String,
173
174 #[builder(default, setter(strip_option(fallback = warehouse_opt)))]
175 warehouse: Option<String>,
176
177 #[builder(default)]
178 props: HashMap<String, String>,
179
180 #[builder(default)]
181 client: Option<Client>,
182}
183
184impl RestCatalogConfig {
185 fn url_prefixed(&self, parts: &[&str]) -> String {
186 [&self.uri, PATH_V1]
187 .into_iter()
188 .chain(self.props.get("prefix").map(|s| &**s))
189 .chain(parts.iter().cloned())
190 .join("/")
191 }
192
193 fn config_endpoint(&self) -> String {
194 [&self.uri, PATH_V1, "config"].join("/")
195 }
196
197 pub(crate) fn get_token_endpoint(&self) -> String {
198 if let Some(oauth2_uri) = self.props.get("oauth2-server-uri") {
199 oauth2_uri.to_string()
200 } else {
201 [&self.uri, PATH_V1, "oauth", "tokens"].join("/")
202 }
203 }
204
205 fn namespaces_endpoint(&self) -> String {
206 self.url_prefixed(&["namespaces"])
207 }
208
209 fn namespace_endpoint(&self, ns: &NamespaceIdent) -> String {
210 self.url_prefixed(&["namespaces", &ns.to_url_string()])
211 }
212
213 fn tables_endpoint(&self, ns: &NamespaceIdent) -> String {
214 self.url_prefixed(&["namespaces", &ns.to_url_string(), "tables"])
215 }
216
217 fn rename_table_endpoint(&self) -> String {
218 self.url_prefixed(&["tables", "rename"])
219 }
220
221 fn register_table_endpoint(&self, ns: &NamespaceIdent) -> String {
222 self.url_prefixed(&["namespaces", &ns.to_url_string(), "register"])
223 }
224
225 fn table_endpoint(&self, table: &TableIdent) -> String {
226 self.url_prefixed(&[
227 "namespaces",
228 &table.namespace.to_url_string(),
229 "tables",
230 &table.name,
231 ])
232 }
233
234 pub(crate) fn client(&self) -> Option<Client> {
236 self.client.clone()
237 }
238
239 pub(crate) fn token(&self) -> Option<String> {
243 self.props.get("token").cloned()
244 }
245
246 pub(crate) fn credential(&self) -> Option<(Option<String>, String)> {
255 let cred = self.props.get("credential")?;
256
257 match cred.split_once(':') {
258 Some((client_id, client_secret)) => {
259 Some((Some(client_id.to_string()), client_secret.to_string()))
260 }
261 None => Some((None, cred.to_string())),
262 }
263 }
264
265 pub(crate) fn extra_headers(&self) -> Result<HeaderMap> {
272 let mut headers = HeaderMap::from_iter([
273 (
274 header::CONTENT_TYPE,
275 HeaderValue::from_static("application/json"),
276 ),
277 (
278 HeaderName::from_static("x-client-version"),
279 HeaderValue::from_static(ICEBERG_REST_SPEC_VERSION),
280 ),
281 (
282 header::USER_AGENT,
283 HeaderValue::from_str(&format!("iceberg-rs/{CARGO_PKG_VERSION}")).unwrap(),
284 ),
285 ]);
286
287 for (key, value) in self
288 .props
289 .iter()
290 .filter_map(|(k, v)| k.strip_prefix("header.").map(|k| (k, v)))
291 {
292 headers.insert(
293 HeaderName::from_str(key).map_err(|e| {
294 Error::new(
295 ErrorKind::DataInvalid,
296 format!("Invalid header name: {key}"),
297 )
298 .with_source(e)
299 })?,
300 HeaderValue::from_str(value).map_err(|e| {
301 Error::new(
302 ErrorKind::DataInvalid,
303 format!("Invalid header value: {value}"),
304 )
305 .with_source(e)
306 })?,
307 );
308 }
309
310 Ok(headers)
311 }
312
313 pub(crate) fn extra_oauth_params(&self) -> HashMap<String, String> {
315 let mut params = HashMap::new();
316
317 if let Some(scope) = self.props.get("scope") {
318 params.insert("scope".to_string(), scope.to_string());
319 } else {
320 params.insert("scope".to_string(), "catalog".to_string());
321 }
322
323 let optional_params = ["audience", "resource"];
324 for param_name in optional_params {
325 if let Some(value) = self.props.get(param_name) {
326 params.insert(param_name.to_string(), value.to_string());
327 }
328 }
329
330 params
331 }
332
333 pub(crate) fn disable_header_redaction(&self) -> bool {
338 self.props
339 .get(REST_CATALOG_PROP_DISABLE_HEADER_REDACTION)
340 .map(|v| v.eq_ignore_ascii_case("true"))
341 .unwrap_or(false)
342 }
343
344 pub(crate) fn merge_with_config(mut self, mut config: CatalogConfig) -> Self {
346 if let Some(uri) = config.overrides.remove("uri") {
347 self.uri = uri;
348 }
349
350 let mut props = config.defaults;
351 props.extend(self.props);
352 props.extend(config.overrides);
353
354 self.props = props;
355 self
356 }
357}
358
359#[derive(Debug)]
360struct RestContext {
361 client: HttpClient,
362 config: RestCatalogConfig,
366 endpoints: HashSet<Endpoint>,
368}
369
370#[derive(Debug)]
372pub struct RestCatalog {
373 user_config: RestCatalogConfig,
377 ctx: OnceCell<RestContext>,
378 storage_factory: Option<Arc<dyn StorageFactory>>,
380 runtime: Runtime,
381 kms_client: Option<Arc<dyn KeyManagementClient>>,
383}
384
385impl RestCatalog {
386 fn new(
388 config: RestCatalogConfig,
389 storage_factory: Option<Arc<dyn StorageFactory>>,
390 runtime: Runtime,
391 kms_client: Option<Arc<dyn KeyManagementClient>>,
392 ) -> Self {
393 Self {
394 user_config: config,
395 ctx: OnceCell::new(),
396 storage_factory,
397 runtime,
398 kms_client,
399 }
400 }
401
402 async fn delete_table(&self, table: &TableIdent, purge: bool) -> Result<()> {
404 let context = self.context().await?;
405
406 let mut request_builder = context
407 .client
408 .request(Method::DELETE, context.config.table_endpoint(table));
409
410 if purge {
411 request_builder = request_builder.query(&[("purgeRequested", "true")]);
412 }
413
414 let request = request_builder.build()?;
415 let http_response = context.client.query_catalog(request).await?;
416
417 match http_response.status() {
418 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
419 StatusCode::NOT_FOUND => Err(Error::new(
420 ErrorKind::TableNotFound,
421 "Tried to drop a table that does not exist",
422 )),
423 _ => Err(deserialize_unexpected_catalog_error(
424 http_response,
425 context.client.disable_header_redaction(),
426 )
427 .await),
428 }
429 }
430
431 async fn context(&self) -> Result<&RestContext> {
433 self.ctx
434 .get_or_try_init(|| async {
435 let client = HttpClient::new(&self.user_config)?;
436 let catalog_config = RestCatalog::load_config(&client, &self.user_config).await?;
437 let endpoints = match &catalog_config.endpoints {
440 Some(advertised) if !advertised.is_empty() => {
441 advertised.iter().cloned().collect()
442 }
443 _ => crate::endpoint::DEFAULT_ENDPOINTS.clone(),
444 };
445 let config = self.user_config.clone().merge_with_config(catalog_config);
446 let client = client.update_with(&config)?;
447
448 Ok(RestContext {
449 config,
450 client,
451 endpoints,
452 })
453 })
454 .await
455 }
456
457 pub(crate) async fn supports_endpoint(&self, endpoint: &Endpoint) -> Result<bool> {
461 Ok(self.context().await?.endpoints.contains(endpoint))
462 }
463
464 async fn check_exists_via_head(&self, context: &RestContext, url: String) -> Result<bool> {
467 let request = context.client.request(Method::HEAD, url).build()?;
468 let http_response = context.client.query_catalog(request).await?;
469
470 match http_response.status() {
471 StatusCode::NO_CONTENT | StatusCode::OK => Ok(true),
472 StatusCode::NOT_FOUND => Ok(false),
473 _ => Err(deserialize_unexpected_catalog_error(
474 http_response,
475 context.client.disable_header_redaction(),
476 )
477 .await),
478 }
479 }
480
481 async fn load_config(
485 client: &HttpClient,
486 user_config: &RestCatalogConfig,
487 ) -> Result<CatalogConfig> {
488 let mut request_builder = client.request(Method::GET, user_config.config_endpoint());
489
490 if let Some(warehouse_location) = &user_config.warehouse {
491 request_builder = request_builder.query(&[("warehouse", warehouse_location)]);
492 }
493
494 let request = request_builder.build()?;
495
496 let http_response = client.query_catalog(request).await?;
497
498 match http_response.status() {
499 StatusCode::OK => deserialize_catalog_response(http_response).await,
500 _ => Err(deserialize_unexpected_catalog_error(
501 http_response,
502 client.disable_header_redaction(),
503 )
504 .await),
505 }
506 }
507
508 async fn load_file_io(
509 &self,
510 metadata_location: Option<&str>,
511 extra_config: Option<HashMap<String, String>>,
512 ) -> Result<FileIO> {
513 let mut props = self.context().await?.config.props.clone();
514 if let Some(config) = extra_config {
515 props.extend(config);
516 }
517
518 let warehouse_path = match self.context().await?.config.warehouse.as_deref() {
521 Some(url) if Url::parse(url).is_ok() => Some(url),
522 Some(_) => None,
523 None => None,
524 };
525
526 if metadata_location.or(warehouse_path).is_none() {
527 return Err(Error::new(
528 ErrorKind::Unexpected,
529 "Unable to load file io, neither warehouse nor metadata location is set!",
530 ));
531 }
532
533 let factory = self
535 .storage_factory
536 .clone()
537 .ok_or_else(|| {
538 Error::new(
539 ErrorKind::Unexpected,
540 "StorageFactory must be provided for RestCatalog. Use `with_storage_factory` to configure it.",
541 )
542 })?;
543
544 let file_io = FileIOBuilder::new(factory).with_props(props).build();
545
546 Ok(file_io)
547 }
548
549 pub async fn invalidate_token(&self) -> Result<()> {
552 self.context().await?.client.invalidate_token().await
553 }
554
555 pub async fn regenerate_token(&self) -> Result<()> {
562 self.context().await?.client.regenerate_token().await
563 }
564}
565
566#[async_trait]
569impl Catalog for RestCatalog {
570 async fn list_namespaces(
571 &self,
572 parent: Option<&NamespaceIdent>,
573 ) -> Result<Vec<NamespaceIdent>> {
574 let context = self.context().await?;
575 let endpoint = context.config.namespaces_endpoint();
576 let mut namespaces = Vec::new();
577 let mut next_token = None;
578
579 loop {
580 let mut request = context.client.request(Method::GET, endpoint.clone());
581
582 if let Some(ns) = parent {
584 request = request.query(&[("parent", ns.to_url_string())]);
585 }
586
587 if let Some(token) = next_token {
588 request = request.query(&[("pageToken", token)]);
589 }
590
591 let http_response = context.client.query_catalog(request.build()?).await?;
592
593 match http_response.status() {
594 StatusCode::OK => {
595 let response =
596 deserialize_catalog_response::<ListNamespaceResponse>(http_response)
597 .await?;
598
599 namespaces.extend(response.namespaces);
600
601 match response.next_page_token {
602 Some(token) => next_token = Some(token),
603 None => break,
604 }
605 }
606 StatusCode::NOT_FOUND => {
607 return Err(Error::new(
608 ErrorKind::NamespaceNotFound,
609 "The parent parameter of the namespace provided does not exist",
610 ));
611 }
612 _ => {
613 return Err(deserialize_unexpected_catalog_error(
614 http_response,
615 context.client.disable_header_redaction(),
616 )
617 .await);
618 }
619 }
620 }
621
622 Ok(namespaces)
623 }
624
625 async fn create_namespace(
626 &self,
627 namespace: &NamespaceIdent,
628 properties: HashMap<String, String>,
629 ) -> Result<Namespace> {
630 let context = self.context().await?;
631
632 let request = context
633 .client
634 .request(Method::POST, context.config.namespaces_endpoint())
635 .json(&CreateNamespaceRequest {
636 namespace: namespace.clone(),
637 properties,
638 })
639 .build()?;
640
641 let http_response = context.client.query_catalog(request).await?;
642
643 match http_response.status() {
644 StatusCode::OK => {
645 let response =
646 deserialize_catalog_response::<NamespaceResponse>(http_response).await?;
647 Ok(Namespace::from(response))
648 }
649 StatusCode::CONFLICT => Err(Error::new(
650 ErrorKind::NamespaceAlreadyExists,
651 "Tried to create a namespace that already exists",
652 )),
653 _ => Err(deserialize_unexpected_catalog_error(
654 http_response,
655 context.client.disable_header_redaction(),
656 )
657 .await),
658 }
659 }
660
661 async fn get_namespace(&self, namespace: &NamespaceIdent) -> Result<Namespace> {
662 let context = self.context().await?;
663
664 let request = context
665 .client
666 .request(Method::GET, context.config.namespace_endpoint(namespace))
667 .build()?;
668
669 let http_response = context.client.query_catalog(request).await?;
670
671 match http_response.status() {
672 StatusCode::OK => {
673 let response =
674 deserialize_catalog_response::<NamespaceResponse>(http_response).await?;
675 Ok(Namespace::from(response))
676 }
677 StatusCode::NOT_FOUND => Err(Error::new(
678 ErrorKind::NamespaceNotFound,
679 "Tried to get a namespace that does not exist",
680 )),
681 _ => Err(deserialize_unexpected_catalog_error(
682 http_response,
683 context.client.disable_header_redaction(),
684 )
685 .await),
686 }
687 }
688
689 async fn namespace_exists(&self, ns: &NamespaceIdent) -> Result<bool> {
690 if !self.supports_endpoint(&V1_NAMESPACE_EXISTS).await? {
695 return match self.get_namespace(ns).await {
696 Ok(_) => Ok(true),
697 Err(e) if e.kind() == ErrorKind::NamespaceNotFound => Ok(false),
698 Err(e) => Err(e),
699 };
700 }
701
702 let context = self.context().await?;
703 self.check_exists_via_head(context, context.config.namespace_endpoint(ns))
704 .await
705 }
706
707 async fn update_namespace(
708 &self,
709 _namespace: &NamespaceIdent,
710 _properties: HashMap<String, String>,
711 ) -> Result<()> {
712 Err(Error::new(
713 ErrorKind::FeatureUnsupported,
714 "Updating namespace not supported yet!",
715 ))
716 }
717
718 async fn drop_namespace(&self, namespace: &NamespaceIdent) -> Result<()> {
719 let context = self.context().await?;
720
721 let request = context
722 .client
723 .request(Method::DELETE, context.config.namespace_endpoint(namespace))
724 .build()?;
725
726 let http_response = context.client.query_catalog(request).await?;
727
728 match http_response.status() {
729 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
730 StatusCode::NOT_FOUND => Err(Error::new(
731 ErrorKind::NamespaceNotFound,
732 "Tried to drop a namespace that does not exist",
733 )),
734 _ => Err(deserialize_unexpected_catalog_error(
735 http_response,
736 context.client.disable_header_redaction(),
737 )
738 .await),
739 }
740 }
741
742 async fn list_tables(&self, namespace: &NamespaceIdent) -> Result<Vec<TableIdent>> {
743 let context = self.context().await?;
744 let endpoint = context.config.tables_endpoint(namespace);
745 let mut identifiers = Vec::new();
746 let mut next_token = None;
747
748 loop {
749 let mut request = context.client.request(Method::GET, endpoint.clone());
750
751 if let Some(token) = next_token {
752 request = request.query(&[("pageToken", token)]);
753 }
754
755 let http_response = context.client.query_catalog(request.build()?).await?;
756
757 match http_response.status() {
758 StatusCode::OK => {
759 let response =
760 deserialize_catalog_response::<ListTablesResponse>(http_response).await?;
761
762 identifiers.extend(response.identifiers);
763
764 match response.next_page_token {
765 Some(token) => next_token = Some(token),
766 None => break,
767 }
768 }
769 StatusCode::NOT_FOUND => {
770 return Err(Error::new(
771 ErrorKind::NamespaceNotFound,
772 "Tried to list tables of a namespace that does not exist",
773 ));
774 }
775 _ => {
776 return Err(deserialize_unexpected_catalog_error(
777 http_response,
778 context.client.disable_header_redaction(),
779 )
780 .await);
781 }
782 }
783 }
784
785 Ok(identifiers)
786 }
787
788 async fn create_table(
795 &self,
796 namespace: &NamespaceIdent,
797 creation: TableCreation,
798 ) -> Result<Table> {
799 let context = self.context().await?;
800
801 let table_ident = TableIdent::new(namespace.clone(), creation.name.clone());
802
803 let request = context
804 .client
805 .request(Method::POST, context.config.tables_endpoint(namespace))
806 .json(&CreateTableRequest {
807 name: creation.name,
808 location: creation.location,
809 schema: creation.schema,
810 partition_spec: creation.partition_spec,
811 write_order: creation.sort_order,
812 stage_create: Some(false),
813 properties: creation.properties,
814 })
815 .build()?;
816
817 let http_response = context.client.query_catalog(request).await?;
818
819 let response = match http_response.status() {
820 StatusCode::OK => {
821 deserialize_catalog_response::<LoadTableResult>(http_response).await?
822 }
823 StatusCode::NOT_FOUND => {
824 return Err(Error::new(
825 ErrorKind::NamespaceNotFound,
826 "Tried to create a table under a namespace that does not exist",
827 ));
828 }
829 StatusCode::CONFLICT => {
830 return Err(Error::new(
831 ErrorKind::TableAlreadyExists,
832 "The table already exists",
833 ));
834 }
835 _ => {
836 return Err(deserialize_unexpected_catalog_error(
837 http_response,
838 context.client.disable_header_redaction(),
839 )
840 .await);
841 }
842 };
843
844 let metadata_location = response.metadata_location.as_ref().ok_or(Error::new(
845 ErrorKind::DataInvalid,
846 "Metadata location missing in `create_table` response!",
847 ))?;
848
849 let config = response
850 .config
851 .into_iter()
852 .chain(self.user_config.props.clone())
853 .collect();
854
855 let file_io = self
856 .load_file_io(Some(metadata_location), Some(config))
857 .await?;
858
859 let mut table_builder = Table::builder()
860 .identifier(table_ident.clone())
861 .file_io(file_io)
862 .metadata(response.metadata)
863 .runtime(self.runtime.clone());
864 if let Some(kms_client) = self.kms_client.clone() {
865 table_builder = table_builder.kms_client(kms_client);
866 }
867
868 if let Some(metadata_location) = response.metadata_location {
869 table_builder.metadata_location(metadata_location).build()
870 } else {
871 table_builder.build()
872 }
873 }
874
875 async fn load_table(&self, table_ident: &TableIdent) -> Result<Table> {
881 let context = self.context().await?;
882
883 let request = context
884 .client
885 .request(Method::GET, context.config.table_endpoint(table_ident))
886 .build()?;
887
888 let http_response = context.client.query_catalog(request).await?;
889
890 let response = match http_response.status() {
891 StatusCode::OK | StatusCode::NOT_MODIFIED => {
892 deserialize_catalog_response::<LoadTableResult>(http_response).await?
893 }
894 StatusCode::NOT_FOUND => {
895 return Err(Error::new(
896 ErrorKind::TableNotFound,
897 "Tried to load a table that does not exist",
898 ));
899 }
900 _ => {
901 return Err(deserialize_unexpected_catalog_error(
902 http_response,
903 context.client.disable_header_redaction(),
904 )
905 .await);
906 }
907 };
908
909 let config = response
910 .config
911 .into_iter()
912 .chain(self.user_config.props.clone())
913 .collect();
914
915 let file_io = self
916 .load_file_io(response.metadata_location.as_deref(), Some(config))
917 .await?;
918
919 let mut table_builder = Table::builder()
920 .identifier(table_ident.clone())
921 .file_io(file_io)
922 .metadata(response.metadata)
923 .runtime(self.runtime.clone());
924 if let Some(kms_client) = self.kms_client.clone() {
925 table_builder = table_builder.kms_client(kms_client);
926 }
927
928 if let Some(metadata_location) = response.metadata_location {
929 table_builder.metadata_location(metadata_location).build()
930 } else {
931 table_builder.build()
932 }
933 }
934
935 async fn drop_table(&self, table: &TableIdent) -> Result<()> {
937 self.delete_table(table, false).await
938 }
939
940 async fn purge_table(&self, table: &TableIdent) -> Result<()> {
943 self.delete_table(table, true).await
944 }
945
946 async fn table_exists(&self, table: &TableIdent) -> Result<bool> {
948 if !self.supports_endpoint(&V1_TABLE_EXISTS).await? {
952 return match self.load_table(table).await {
953 Ok(_) => Ok(true),
954 Err(e) if e.kind() == ErrorKind::TableNotFound => Ok(false),
955 Err(e) => Err(e),
956 };
957 }
958
959 let context = self.context().await?;
960 self.check_exists_via_head(context, context.config.table_endpoint(table))
961 .await
962 }
963
964 async fn rename_table(&self, src: &TableIdent, dest: &TableIdent) -> Result<()> {
966 let context = self.context().await?;
967
968 let request = context
969 .client
970 .request(Method::POST, context.config.rename_table_endpoint())
971 .json(&RenameTableRequest {
972 source: src.clone(),
973 destination: dest.clone(),
974 })
975 .build()?;
976
977 let http_response = context.client.query_catalog(request).await?;
978
979 match http_response.status() {
980 StatusCode::NO_CONTENT | StatusCode::OK => Ok(()),
981 StatusCode::NOT_FOUND => Err(Error::new(
982 ErrorKind::TableNotFound,
983 "Tried to rename a table that does not exist (is the namespace correct?)",
984 )),
985 StatusCode::CONFLICT => Err(Error::new(
986 ErrorKind::TableAlreadyExists,
987 "Tried to rename a table to a name that already exists",
988 )),
989 _ => Err(deserialize_unexpected_catalog_error(
990 http_response,
991 context.client.disable_header_redaction(),
992 )
993 .await),
994 }
995 }
996
997 async fn register_table(
998 &self,
999 table_ident: &TableIdent,
1000 metadata_location: String,
1001 ) -> Result<Table> {
1002 let context = self.context().await?;
1003
1004 let request = context
1005 .client
1006 .request(
1007 Method::POST,
1008 context
1009 .config
1010 .register_table_endpoint(table_ident.namespace()),
1011 )
1012 .json(&RegisterTableRequest {
1013 name: table_ident.name.clone(),
1014 metadata_location: metadata_location.clone(),
1015 overwrite: Some(false),
1016 })
1017 .build()?;
1018
1019 let http_response = context.client.query_catalog(request).await?;
1020
1021 let response: LoadTableResult = match http_response.status() {
1022 StatusCode::OK => {
1023 deserialize_catalog_response::<LoadTableResult>(http_response).await?
1024 }
1025 StatusCode::NOT_FOUND => {
1026 return Err(Error::new(
1027 ErrorKind::NamespaceNotFound,
1028 "The namespace specified does not exist.",
1029 ));
1030 }
1031 StatusCode::CONFLICT => {
1032 return Err(Error::new(
1033 ErrorKind::TableAlreadyExists,
1034 "The given table already exists.",
1035 ));
1036 }
1037 _ => {
1038 return Err(deserialize_unexpected_catalog_error(
1039 http_response,
1040 context.client.disable_header_redaction(),
1041 )
1042 .await);
1043 }
1044 };
1045
1046 let metadata_location = response.metadata_location.as_ref().ok_or(Error::new(
1047 ErrorKind::DataInvalid,
1048 "Metadata location missing in `register_table` response!",
1049 ))?;
1050
1051 let file_io = self.load_file_io(Some(metadata_location), None).await?;
1052
1053 let mut table_builder = Table::builder()
1054 .identifier(table_ident.clone())
1055 .file_io(file_io)
1056 .metadata(response.metadata)
1057 .metadata_location(metadata_location.clone())
1058 .runtime(self.runtime.clone());
1059 if let Some(kms_client) = self.kms_client.clone() {
1060 table_builder = table_builder.kms_client(kms_client);
1061 }
1062 table_builder.build()
1063 }
1064
1065 async fn update_table(&self, mut commit: TableCommit) -> Result<Table> {
1066 let context = self.context().await?;
1067
1068 let request = context
1069 .client
1070 .request(
1071 Method::POST,
1072 context.config.table_endpoint(commit.identifier()),
1073 )
1074 .json(&CommitTableRequest {
1075 identifier: Some(commit.identifier().clone()),
1076 requirements: commit.take_requirements(),
1077 updates: commit.take_updates(),
1078 })
1079 .build()?;
1080
1081 let http_response = context.client.query_catalog(request).await?;
1082
1083 let response: CommitTableResponse = match http_response.status() {
1084 StatusCode::OK => deserialize_catalog_response(http_response).await?,
1085 StatusCode::NOT_FOUND => {
1086 return Err(Error::new(
1087 ErrorKind::TableNotFound,
1088 "Tried to update a table that does not exist",
1089 ));
1090 }
1091 StatusCode::CONFLICT => {
1092 return Err(Error::new(
1093 ErrorKind::CatalogCommitConflicts,
1094 "CatalogCommitConflicts, one or more requirements failed. The client may retry.",
1095 )
1096 .with_retryable(true));
1097 }
1098 StatusCode::INTERNAL_SERVER_ERROR => {
1099 return Err(Error::new(
1100 ErrorKind::Unexpected,
1101 "An unknown server-side problem occurred; the commit state is unknown.",
1102 ));
1103 }
1104 StatusCode::BAD_GATEWAY => {
1105 return Err(Error::new(
1106 ErrorKind::Unexpected,
1107 "A gateway or proxy received an invalid response from the upstream server; the commit state is unknown.",
1108 ));
1109 }
1110 StatusCode::GATEWAY_TIMEOUT => {
1111 return Err(Error::new(
1112 ErrorKind::Unexpected,
1113 "A server-side gateway timeout occurred; the commit state is unknown.",
1114 ));
1115 }
1116 _ => {
1117 return Err(deserialize_unexpected_catalog_error(
1118 http_response,
1119 context.client.disable_header_redaction(),
1120 )
1121 .await);
1122 }
1123 };
1124
1125 let file_io = self
1126 .load_file_io(Some(&response.metadata_location), None)
1127 .await?;
1128
1129 let mut table_builder = Table::builder()
1130 .identifier(commit.identifier().clone())
1131 .file_io(file_io)
1132 .metadata(response.metadata)
1133 .metadata_location(response.metadata_location)
1134 .runtime(self.runtime.clone());
1135 if let Some(kms_client) = self.kms_client.clone() {
1136 table_builder = table_builder.kms_client(kms_client);
1137 }
1138 table_builder.build()
1139 }
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144 use std::fs::File;
1145 use std::io::BufReader;
1146 use std::sync::Arc;
1147
1148 use chrono::{TimeZone, Utc};
1149 use iceberg::io::LocalFsStorageFactory;
1150 use iceberg::spec::{
1151 FormatVersion, NestedField, NullOrder, Operation, PrimitiveType, Schema, Snapshot,
1152 SnapshotLog, SortDirection, SortField, SortOrder, Summary, Transform, Type,
1153 UnboundPartitionField, UnboundPartitionSpec,
1154 };
1155 use iceberg::test_utils::test_runtime;
1156 use iceberg::transaction::{ApplyTransactionAction, Transaction};
1157 use mockito::{Mock, Server, ServerGuard};
1158 use serde_json::json;
1159 use uuid::uuid;
1160
1161 use super::*;
1162
1163 #[tokio::test]
1164 async fn test_update_config() {
1165 let mut server = Server::new_async().await;
1166
1167 let config_mock = server
1168 .mock("GET", "/v1/config")
1169 .with_status(200)
1170 .with_body(
1171 r#"{
1172 "overrides": {
1173 "warehouse": "s3://iceberg-catalog"
1174 },
1175 "defaults": {}
1176 }"#,
1177 )
1178 .create_async()
1179 .await;
1180
1181 let catalog = RestCatalog::new(
1182 RestCatalogConfig::builder().uri(server.url()).build(),
1183 Some(Arc::new(LocalFsStorageFactory)),
1184 Runtime::current(),
1185 None,
1186 );
1187
1188 assert_eq!(
1189 catalog
1190 .context()
1191 .await
1192 .unwrap()
1193 .config
1194 .props
1195 .get("warehouse"),
1196 Some(&"s3://iceberg-catalog".to_string())
1197 );
1198
1199 config_mock.assert_async().await;
1200 }
1201
1202 async fn create_config_mock(server: &mut ServerGuard) -> Mock {
1203 server
1204 .mock("GET", "/v1/config")
1205 .with_status(200)
1206 .with_body(
1207 r#"{
1208 "overrides": {
1209 "warehouse": "s3://iceberg-catalog"
1210 },
1211 "defaults": {}
1212 }"#,
1213 )
1214 .create_async()
1215 .await
1216 }
1217
1218 async fn create_config_mock_with_exists_endpoints(server: &mut ServerGuard) -> Mock {
1221 server
1222 .mock("GET", "/v1/config")
1223 .with_status(200)
1224 .with_body(
1225 r#"{
1226 "overrides": { "warehouse": "s3://iceberg-catalog" },
1227 "defaults": {},
1228 "endpoints": [
1229 "HEAD /v1/{prefix}/namespaces/{namespace}",
1230 "HEAD /v1/{prefix}/namespaces/{namespace}/tables/{table}"
1231 ]
1232 }"#,
1233 )
1234 .create_async()
1235 .await
1236 }
1237
1238 #[tokio::test]
1239 async fn test_config_advertised_endpoints() {
1240 let mut server = Server::new_async().await;
1241
1242 let config_mock = server
1243 .mock("GET", "/v1/config")
1244 .with_status(200)
1245 .with_body(
1246 r#"{
1247 "overrides": {},
1248 "defaults": {},
1249 "endpoints": [
1250 "GET /v1/{prefix}/namespaces",
1251 "POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan"
1252 ]
1253 }"#,
1254 )
1255 .create_async()
1256 .await;
1257
1258 let catalog = RestCatalog::new(
1259 RestCatalogConfig::builder().uri(server.url()).build(),
1260 Some(Arc::new(LocalFsStorageFactory)),
1261 Runtime::current(),
1262 None,
1263 );
1264
1265 let plan = "POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan"
1266 .parse::<Endpoint>()
1267 .unwrap();
1268 assert!(catalog.supports_endpoint(&plan).await.unwrap());
1269 let delete_ns = "DELETE /v1/{prefix}/namespaces/{namespace}"
1271 .parse::<Endpoint>()
1272 .unwrap();
1273 assert!(!catalog.supports_endpoint(&delete_ns).await.unwrap());
1274
1275 config_mock.assert_async().await;
1276 }
1277
1278 #[tokio::test]
1279 async fn test_config_without_endpoints_falls_back_to_default_set() {
1280 let mut server = Server::new_async().await;
1281
1282 let config_mock = server
1283 .mock("GET", "/v1/config")
1284 .with_status(200)
1285 .with_body(r#"{ "overrides": {}, "defaults": {} }"#)
1286 .create_async()
1287 .await;
1288
1289 let catalog = RestCatalog::new(
1290 RestCatalogConfig::builder().uri(server.url()).build(),
1291 Some(Arc::new(LocalFsStorageFactory)),
1292 Runtime::current(),
1293 None,
1294 );
1295
1296 let load_table = "GET /v1/{prefix}/namespaces/{namespace}/tables/{table}"
1299 .parse::<Endpoint>()
1300 .unwrap();
1301 assert!(catalog.supports_endpoint(&load_table).await.unwrap());
1302 let plan = "POST /v1/{prefix}/namespaces/{namespace}/tables/{table}/plan"
1304 .parse::<Endpoint>()
1305 .unwrap();
1306 assert!(!catalog.supports_endpoint(&plan).await.unwrap());
1307
1308 config_mock.assert_async().await;
1309 }
1310
1311 #[tokio::test]
1312 async fn test_config_with_empty_endpoints_falls_back_to_default_set() {
1313 let mut server = Server::new_async().await;
1314
1315 let config_mock = server
1318 .mock("GET", "/v1/config")
1319 .with_status(200)
1320 .with_body(r#"{ "overrides": {}, "defaults": {}, "endpoints": [] }"#)
1321 .create_async()
1322 .await;
1323
1324 let catalog = RestCatalog::new(
1325 RestCatalogConfig::builder().uri(server.url()).build(),
1326 Some(Arc::new(LocalFsStorageFactory)),
1327 Runtime::current(),
1328 None,
1329 );
1330
1331 let load_table = "GET /v1/{prefix}/namespaces/{namespace}/tables/{table}"
1332 .parse::<Endpoint>()
1333 .unwrap();
1334 assert!(catalog.supports_endpoint(&load_table).await.unwrap());
1335
1336 config_mock.assert_async().await;
1337 }
1338
1339 async fn create_oauth_mock(server: &mut ServerGuard) -> Mock {
1340 create_oauth_mock_with_path(server, "/v1/oauth/tokens", "ey000000000000", 200).await
1341 }
1342
1343 async fn create_oauth_mock_with_path(
1344 server: &mut ServerGuard,
1345 path: &str,
1346 token: &str,
1347 status: usize,
1348 ) -> Mock {
1349 let body = format!(
1350 r#"{{
1351 "access_token": "{token}",
1352 "token_type": "Bearer",
1353 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1354 "expires_in": 86400
1355 }}"#
1356 );
1357 server
1358 .mock("POST", path)
1359 .with_status(status)
1360 .with_body(body)
1361 .expect(1)
1362 .create_async()
1363 .await
1364 }
1365
1366 #[tokio::test]
1367 async fn test_oauth() {
1368 let mut server = Server::new_async().await;
1369 let oauth_mock = create_oauth_mock(&mut server).await;
1370 let config_mock = create_config_mock(&mut server).await;
1371
1372 let mut props = HashMap::new();
1373 props.insert("credential".to_string(), "client1:secret1".to_string());
1374
1375 let catalog = RestCatalog::new(
1376 RestCatalogConfig::builder()
1377 .uri(server.url())
1378 .props(props)
1379 .build(),
1380 Some(Arc::new(LocalFsStorageFactory)),
1381 Runtime::current(),
1382 None,
1383 );
1384
1385 let token = catalog.context().await.unwrap().client.token().await;
1386 oauth_mock.assert_async().await;
1387 config_mock.assert_async().await;
1388 assert_eq!(token, Some("ey000000000000".to_string()));
1389 }
1390
1391 #[tokio::test]
1392 async fn test_oauth_with_optional_param() {
1393 let mut props = HashMap::new();
1394 props.insert("credential".to_string(), "client1:secret1".to_string());
1395 props.insert("scope".to_string(), "custom_scope".to_string());
1396 props.insert("audience".to_string(), "custom_audience".to_string());
1397 props.insert("resource".to_string(), "custom_resource".to_string());
1398
1399 let mut server = Server::new_async().await;
1400 let oauth_mock = server
1401 .mock("POST", "/v1/oauth/tokens")
1402 .match_body(mockito::Matcher::Regex("scope=custom_scope".to_string()))
1403 .match_body(mockito::Matcher::Regex(
1404 "audience=custom_audience".to_string(),
1405 ))
1406 .match_body(mockito::Matcher::Regex(
1407 "resource=custom_resource".to_string(),
1408 ))
1409 .with_status(200)
1410 .with_body(
1411 r#"{
1412 "access_token": "ey000000000000",
1413 "token_type": "Bearer",
1414 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1415 "expires_in": 86400
1416 }"#,
1417 )
1418 .expect(1)
1419 .create_async()
1420 .await;
1421
1422 let config_mock = create_config_mock(&mut server).await;
1423
1424 let catalog = RestCatalog::new(
1425 RestCatalogConfig::builder()
1426 .uri(server.url())
1427 .props(props)
1428 .build(),
1429 Some(Arc::new(LocalFsStorageFactory)),
1430 Runtime::current(),
1431 None,
1432 );
1433
1434 let token = catalog.context().await.unwrap().client.token().await;
1435
1436 oauth_mock.assert_async().await;
1437 config_mock.assert_async().await;
1438 assert_eq!(token, Some("ey000000000000".to_string()));
1439 }
1440
1441 #[tokio::test]
1442 async fn test_invalidate_token() {
1443 let mut server = Server::new_async().await;
1444 let oauth_mock = create_oauth_mock(&mut server).await;
1445 let config_mock = create_config_mock(&mut server).await;
1446
1447 let mut props = HashMap::new();
1448 props.insert("credential".to_string(), "client1:secret1".to_string());
1449
1450 let catalog = RestCatalog::new(
1451 RestCatalogConfig::builder()
1452 .uri(server.url())
1453 .props(props)
1454 .build(),
1455 Some(Arc::new(LocalFsStorageFactory)),
1456 Runtime::current(),
1457 None,
1458 );
1459
1460 let token = catalog.context().await.unwrap().client.token().await;
1461 oauth_mock.assert_async().await;
1462 config_mock.assert_async().await;
1463 assert_eq!(token, Some("ey000000000000".to_string()));
1464
1465 let oauth_mock =
1466 create_oauth_mock_with_path(&mut server, "/v1/oauth/tokens", "ey000000000001", 200)
1467 .await;
1468 catalog.invalidate_token().await.unwrap();
1469 let token = catalog.context().await.unwrap().client.token().await;
1470 oauth_mock.assert_async().await;
1471 assert_eq!(token, Some("ey000000000001".to_string()));
1472 }
1473
1474 #[tokio::test]
1475 async fn test_invalidate_token_failing_request() {
1476 let mut server = Server::new_async().await;
1477 let oauth_mock = create_oauth_mock(&mut server).await;
1478 let config_mock = create_config_mock(&mut server).await;
1479
1480 let mut props = HashMap::new();
1481 props.insert("credential".to_string(), "client1:secret1".to_string());
1482
1483 let catalog = RestCatalog::new(
1484 RestCatalogConfig::builder()
1485 .uri(server.url())
1486 .props(props)
1487 .build(),
1488 Some(Arc::new(LocalFsStorageFactory)),
1489 Runtime::current(),
1490 None,
1491 );
1492
1493 let token = catalog.context().await.unwrap().client.token().await;
1494 oauth_mock.assert_async().await;
1495 config_mock.assert_async().await;
1496 assert_eq!(token, Some("ey000000000000".to_string()));
1497
1498 let oauth_mock =
1499 create_oauth_mock_with_path(&mut server, "/v1/oauth/tokens", "ey000000000001", 500)
1500 .await;
1501 catalog.invalidate_token().await.unwrap();
1502 let token = catalog.context().await.unwrap().client.token().await;
1503 oauth_mock.assert_async().await;
1504 assert_eq!(token, None);
1505 }
1506
1507 #[tokio::test]
1508 async fn test_regenerate_token() {
1509 let mut server = Server::new_async().await;
1510 let oauth_mock = create_oauth_mock(&mut server).await;
1511 let config_mock = create_config_mock(&mut server).await;
1512
1513 let mut props = HashMap::new();
1514 props.insert("credential".to_string(), "client1:secret1".to_string());
1515
1516 let catalog = RestCatalog::new(
1517 RestCatalogConfig::builder()
1518 .uri(server.url())
1519 .props(props)
1520 .build(),
1521 Some(Arc::new(LocalFsStorageFactory)),
1522 Runtime::current(),
1523 None,
1524 );
1525
1526 let token = catalog.context().await.unwrap().client.token().await;
1527 oauth_mock.assert_async().await;
1528 config_mock.assert_async().await;
1529 assert_eq!(token, Some("ey000000000000".to_string()));
1530
1531 let oauth_mock =
1532 create_oauth_mock_with_path(&mut server, "/v1/oauth/tokens", "ey000000000001", 200)
1533 .await;
1534 catalog.regenerate_token().await.unwrap();
1535 oauth_mock.assert_async().await;
1536 let token = catalog.context().await.unwrap().client.token().await;
1537 assert_eq!(token, Some("ey000000000001".to_string()));
1538 }
1539
1540 #[tokio::test]
1541 async fn test_regenerate_token_failing_request() {
1542 let mut server = Server::new_async().await;
1543 let oauth_mock = create_oauth_mock(&mut server).await;
1544 let config_mock = create_config_mock(&mut server).await;
1545
1546 let mut props = HashMap::new();
1547 props.insert("credential".to_string(), "client1:secret1".to_string());
1548
1549 let catalog = RestCatalog::new(
1550 RestCatalogConfig::builder()
1551 .uri(server.url())
1552 .props(props)
1553 .build(),
1554 Some(Arc::new(LocalFsStorageFactory)),
1555 Runtime::current(),
1556 None,
1557 );
1558
1559 let token = catalog.context().await.unwrap().client.token().await;
1560 oauth_mock.assert_async().await;
1561 config_mock.assert_async().await;
1562 assert_eq!(token, Some("ey000000000000".to_string()));
1563
1564 let oauth_mock =
1565 create_oauth_mock_with_path(&mut server, "/v1/oauth/tokens", "ey000000000001", 500)
1566 .await;
1567 let invalidate_result = catalog.regenerate_token().await;
1568 assert!(invalidate_result.is_err());
1569 oauth_mock.assert_async().await;
1570 let token = catalog.context().await.unwrap().client.token().await;
1571
1572 assert_eq!(token, Some("ey000000000000".to_string()));
1574 }
1575
1576 #[tokio::test]
1577 async fn test_http_headers() {
1578 let server = Server::new_async().await;
1579 let mut props = HashMap::new();
1580 props.insert("credential".to_string(), "client1:secret1".to_string());
1581
1582 let config = RestCatalogConfig::builder()
1583 .uri(server.url())
1584 .props(props)
1585 .build();
1586 let headers: HeaderMap = config.extra_headers().unwrap();
1587
1588 let expected_headers = HeaderMap::from_iter([
1589 (
1590 header::CONTENT_TYPE,
1591 HeaderValue::from_static("application/json"),
1592 ),
1593 (
1594 HeaderName::from_static("x-client-version"),
1595 HeaderValue::from_static(ICEBERG_REST_SPEC_VERSION),
1596 ),
1597 (
1598 header::USER_AGENT,
1599 HeaderValue::from_str(&format!("iceberg-rs/{CARGO_PKG_VERSION}")).unwrap(),
1600 ),
1601 ]);
1602 assert_eq!(headers, expected_headers);
1603 }
1604
1605 #[tokio::test]
1606 async fn test_http_headers_with_custom_headers() {
1607 let server = Server::new_async().await;
1608 let mut props = HashMap::new();
1609 props.insert("credential".to_string(), "client1:secret1".to_string());
1610 props.insert(
1611 "header.content-type".to_string(),
1612 "application/yaml".to_string(),
1613 );
1614 props.insert(
1615 "header.customized-header".to_string(),
1616 "some/value".to_string(),
1617 );
1618
1619 let config = RestCatalogConfig::builder()
1620 .uri(server.url())
1621 .props(props)
1622 .build();
1623 let headers: HeaderMap = config.extra_headers().unwrap();
1624
1625 let expected_headers = HeaderMap::from_iter([
1626 (
1627 header::CONTENT_TYPE,
1628 HeaderValue::from_static("application/yaml"),
1629 ),
1630 (
1631 HeaderName::from_static("x-client-version"),
1632 HeaderValue::from_static(ICEBERG_REST_SPEC_VERSION),
1633 ),
1634 (
1635 header::USER_AGENT,
1636 HeaderValue::from_str(&format!("iceberg-rs/{CARGO_PKG_VERSION}")).unwrap(),
1637 ),
1638 (
1639 HeaderName::from_static("customized-header"),
1640 HeaderValue::from_static("some/value"),
1641 ),
1642 ]);
1643 assert_eq!(headers, expected_headers);
1644 }
1645
1646 #[tokio::test]
1647 async fn test_oauth_with_oauth2_server_uri() {
1648 let mut server = Server::new_async().await;
1649 let config_mock = create_config_mock(&mut server).await;
1650
1651 let mut auth_server = Server::new_async().await;
1652 let auth_server_path = "/some/path";
1653 let oauth_mock =
1654 create_oauth_mock_with_path(&mut auth_server, auth_server_path, "ey000000000000", 200)
1655 .await;
1656
1657 let mut props = HashMap::new();
1658 props.insert("credential".to_string(), "client1:secret1".to_string());
1659 props.insert(
1660 "oauth2-server-uri".to_string(),
1661 format!("{}{}", auth_server.url(), auth_server_path).to_string(),
1662 );
1663
1664 let catalog = RestCatalog::new(
1665 RestCatalogConfig::builder()
1666 .uri(server.url())
1667 .props(props)
1668 .build(),
1669 Some(Arc::new(LocalFsStorageFactory)),
1670 Runtime::current(),
1671 None,
1672 );
1673
1674 let token = catalog.context().await.unwrap().client.token().await;
1675
1676 oauth_mock.assert_async().await;
1677 config_mock.assert_async().await;
1678 assert_eq!(token, Some("ey000000000000".to_string()));
1679 }
1680
1681 #[tokio::test]
1682 async fn test_config_override() {
1683 let mut server = Server::new_async().await;
1684 let mut redirect_server = Server::new_async().await;
1685 let new_uri = redirect_server.url();
1686
1687 let config_mock = server
1688 .mock("GET", "/v1/config")
1689 .with_status(200)
1690 .with_body(
1691 json!(
1692 {
1693 "overrides": {
1694 "uri": new_uri,
1695 "warehouse": "s3://iceberg-catalog",
1696 "prefix": "ice/warehouses/my"
1697 },
1698 "defaults": {},
1699 }
1700 )
1701 .to_string(),
1702 )
1703 .create_async()
1704 .await;
1705
1706 let list_ns_mock = redirect_server
1707 .mock("GET", "/v1/ice/warehouses/my/namespaces")
1708 .with_body(
1709 r#"{
1710 "namespaces": []
1711 }"#,
1712 )
1713 .create_async()
1714 .await;
1715
1716 let catalog = RestCatalog::new(
1717 RestCatalogConfig::builder().uri(server.url()).build(),
1718 Some(Arc::new(LocalFsStorageFactory)),
1719 Runtime::current(),
1720 None,
1721 );
1722
1723 let _namespaces = catalog.list_namespaces(None).await.unwrap();
1724
1725 config_mock.assert_async().await;
1726 list_ns_mock.assert_async().await;
1727 }
1728
1729 #[tokio::test]
1730 async fn test_list_namespace() {
1731 let mut server = Server::new_async().await;
1732
1733 let config_mock = create_config_mock(&mut server).await;
1734
1735 let list_ns_mock = server
1736 .mock("GET", "/v1/namespaces")
1737 .with_body(
1738 r#"{
1739 "namespaces": [
1740 ["ns1", "ns11"],
1741 ["ns2"]
1742 ]
1743 }"#,
1744 )
1745 .create_async()
1746 .await;
1747
1748 let catalog = RestCatalog::new(
1749 RestCatalogConfig::builder().uri(server.url()).build(),
1750 Some(Arc::new(LocalFsStorageFactory)),
1751 Runtime::current(),
1752 None,
1753 );
1754
1755 let namespaces = catalog.list_namespaces(None).await.unwrap();
1756
1757 let expected_ns = vec![
1758 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
1759 NamespaceIdent::from_vec(vec!["ns2".to_string()]).unwrap(),
1760 ];
1761
1762 assert_eq!(expected_ns, namespaces);
1763
1764 config_mock.assert_async().await;
1765 list_ns_mock.assert_async().await;
1766 }
1767
1768 #[tokio::test]
1769 async fn test_list_namespace_with_pagination() {
1770 let mut server = Server::new_async().await;
1771
1772 let config_mock = create_config_mock(&mut server).await;
1773
1774 let list_ns_mock_page1 = server
1775 .mock("GET", "/v1/namespaces")
1776 .with_body(
1777 r#"{
1778 "namespaces": [
1779 ["ns1", "ns11"],
1780 ["ns2"]
1781 ],
1782 "next-page-token": "token123"
1783 }"#,
1784 )
1785 .create_async()
1786 .await;
1787
1788 let list_ns_mock_page2 = server
1789 .mock("GET", "/v1/namespaces?pageToken=token123")
1790 .with_body(
1791 r#"{
1792 "namespaces": [
1793 ["ns3"],
1794 ["ns4", "ns41"]
1795 ]
1796 }"#,
1797 )
1798 .create_async()
1799 .await;
1800
1801 let catalog = RestCatalog::new(
1802 RestCatalogConfig::builder().uri(server.url()).build(),
1803 Some(Arc::new(LocalFsStorageFactory)),
1804 Runtime::current(),
1805 None,
1806 );
1807
1808 let namespaces = catalog.list_namespaces(None).await.unwrap();
1809
1810 let expected_ns = vec![
1811 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
1812 NamespaceIdent::from_vec(vec!["ns2".to_string()]).unwrap(),
1813 NamespaceIdent::from_vec(vec!["ns3".to_string()]).unwrap(),
1814 NamespaceIdent::from_vec(vec!["ns4".to_string(), "ns41".to_string()]).unwrap(),
1815 ];
1816
1817 assert_eq!(expected_ns, namespaces);
1818
1819 config_mock.assert_async().await;
1820 list_ns_mock_page1.assert_async().await;
1821 list_ns_mock_page2.assert_async().await;
1822 }
1823
1824 #[tokio::test]
1825 async fn test_list_namespace_with_multiple_pages() {
1826 let mut server = Server::new_async().await;
1827
1828 let config_mock = create_config_mock(&mut server).await;
1829
1830 let list_ns_mock_page1 = server
1832 .mock("GET", "/v1/namespaces")
1833 .with_body(
1834 r#"{
1835 "namespaces": [
1836 ["ns1", "ns11"],
1837 ["ns2"]
1838 ],
1839 "next-page-token": "page2"
1840 }"#,
1841 )
1842 .create_async()
1843 .await;
1844
1845 let list_ns_mock_page2 = server
1847 .mock("GET", "/v1/namespaces?pageToken=page2")
1848 .with_body(
1849 r#"{
1850 "namespaces": [
1851 ["ns3"],
1852 ["ns4", "ns41"]
1853 ],
1854 "next-page-token": "page3"
1855 }"#,
1856 )
1857 .create_async()
1858 .await;
1859
1860 let list_ns_mock_page3 = server
1862 .mock("GET", "/v1/namespaces?pageToken=page3")
1863 .with_body(
1864 r#"{
1865 "namespaces": [
1866 ["ns5", "ns51", "ns511"]
1867 ],
1868 "next-page-token": "page4"
1869 }"#,
1870 )
1871 .create_async()
1872 .await;
1873
1874 let list_ns_mock_page4 = server
1876 .mock("GET", "/v1/namespaces?pageToken=page4")
1877 .with_body(
1878 r#"{
1879 "namespaces": [
1880 ["ns6"],
1881 ["ns7"]
1882 ],
1883 "next-page-token": "page5"
1884 }"#,
1885 )
1886 .create_async()
1887 .await;
1888
1889 let list_ns_mock_page5 = server
1891 .mock("GET", "/v1/namespaces?pageToken=page5")
1892 .with_body(
1893 r#"{
1894 "namespaces": [
1895 ["ns8", "ns81"]
1896 ]
1897 }"#,
1898 )
1899 .create_async()
1900 .await;
1901
1902 let catalog = RestCatalog::new(
1903 RestCatalogConfig::builder().uri(server.url()).build(),
1904 Some(Arc::new(LocalFsStorageFactory)),
1905 Runtime::current(),
1906 None,
1907 );
1908
1909 let namespaces = catalog.list_namespaces(None).await.unwrap();
1910
1911 let expected_ns = vec![
1912 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
1913 NamespaceIdent::from_vec(vec!["ns2".to_string()]).unwrap(),
1914 NamespaceIdent::from_vec(vec!["ns3".to_string()]).unwrap(),
1915 NamespaceIdent::from_vec(vec!["ns4".to_string(), "ns41".to_string()]).unwrap(),
1916 NamespaceIdent::from_vec(vec![
1917 "ns5".to_string(),
1918 "ns51".to_string(),
1919 "ns511".to_string(),
1920 ])
1921 .unwrap(),
1922 NamespaceIdent::from_vec(vec!["ns6".to_string()]).unwrap(),
1923 NamespaceIdent::from_vec(vec!["ns7".to_string()]).unwrap(),
1924 NamespaceIdent::from_vec(vec!["ns8".to_string(), "ns81".to_string()]).unwrap(),
1925 ];
1926
1927 assert_eq!(expected_ns, namespaces);
1928
1929 config_mock.assert_async().await;
1931 list_ns_mock_page1.assert_async().await;
1932 list_ns_mock_page2.assert_async().await;
1933 list_ns_mock_page3.assert_async().await;
1934 list_ns_mock_page4.assert_async().await;
1935 list_ns_mock_page5.assert_async().await;
1936 }
1937
1938 #[tokio::test]
1939 async fn test_create_namespace() {
1940 let mut server = Server::new_async().await;
1941
1942 let config_mock = create_config_mock(&mut server).await;
1943
1944 let create_ns_mock = server
1945 .mock("POST", "/v1/namespaces")
1946 .with_body(
1947 r#"{
1948 "namespace": [ "ns1", "ns11"],
1949 "properties" : {
1950 "key1": "value1"
1951 }
1952 }"#,
1953 )
1954 .create_async()
1955 .await;
1956
1957 let catalog = RestCatalog::new(
1958 RestCatalogConfig::builder().uri(server.url()).build(),
1959 Some(Arc::new(LocalFsStorageFactory)),
1960 Runtime::current(),
1961 None,
1962 );
1963
1964 let namespaces = catalog
1965 .create_namespace(
1966 &NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
1967 HashMap::from([("key1".to_string(), "value1".to_string())]),
1968 )
1969 .await
1970 .unwrap();
1971
1972 let expected_ns = Namespace::with_properties(
1973 NamespaceIdent::from_vec(vec!["ns1".to_string(), "ns11".to_string()]).unwrap(),
1974 HashMap::from([("key1".to_string(), "value1".to_string())]),
1975 );
1976
1977 assert_eq!(expected_ns, namespaces);
1978
1979 config_mock.assert_async().await;
1980 create_ns_mock.assert_async().await;
1981 }
1982
1983 #[tokio::test]
1984 async fn test_get_namespace() {
1985 let mut server = Server::new_async().await;
1986
1987 let config_mock = create_config_mock(&mut server).await;
1988
1989 let get_ns_mock = server
1990 .mock("GET", "/v1/namespaces/ns1")
1991 .with_body(
1992 r#"{
1993 "namespace": [ "ns1"],
1994 "properties" : {
1995 "key1": "value1"
1996 }
1997 }"#,
1998 )
1999 .create_async()
2000 .await;
2001
2002 let catalog = RestCatalog::new(
2003 RestCatalogConfig::builder().uri(server.url()).build(),
2004 Some(Arc::new(LocalFsStorageFactory)),
2005 Runtime::current(),
2006 None,
2007 );
2008
2009 let namespaces = catalog
2010 .get_namespace(&NamespaceIdent::new("ns1".to_string()))
2011 .await
2012 .unwrap();
2013
2014 let expected_ns = Namespace::with_properties(
2015 NamespaceIdent::new("ns1".to_string()),
2016 HashMap::from([("key1".to_string(), "value1".to_string())]),
2017 );
2018
2019 assert_eq!(expected_ns, namespaces);
2020
2021 config_mock.assert_async().await;
2022 get_ns_mock.assert_async().await;
2023 }
2024
2025 #[tokio::test]
2026 async fn check_namespace_exists() {
2027 let mut server = Server::new_async().await;
2028
2029 let config_mock = create_config_mock_with_exists_endpoints(&mut server).await;
2030
2031 let get_ns_mock = server
2032 .mock("HEAD", "/v1/namespaces/ns1")
2033 .with_status(204)
2034 .create_async()
2035 .await;
2036
2037 let catalog = RestCatalog::new(
2038 RestCatalogConfig::builder().uri(server.url()).build(),
2039 Some(Arc::new(LocalFsStorageFactory)),
2040 Runtime::current(),
2041 None,
2042 );
2043
2044 assert!(
2045 catalog
2046 .namespace_exists(&NamespaceIdent::new("ns1".to_string()))
2047 .await
2048 .unwrap()
2049 );
2050
2051 config_mock.assert_async().await;
2052 get_ns_mock.assert_async().await;
2053 }
2054
2055 #[tokio::test]
2056 async fn test_namespace_exists_falls_back_to_get_when_head_not_advertised() {
2057 let mut server = Server::new_async().await;
2058
2059 let config_mock = create_config_mock(&mut server).await;
2062 let get_ns_mock = server
2063 .mock("GET", "/v1/namespaces/ns1")
2064 .with_status(200)
2065 .with_body(
2066 r#"{
2067 "namespace": ["ns1"],
2068 "properties": {}
2069 }"#,
2070 )
2071 .create_async()
2072 .await;
2073
2074 let catalog = RestCatalog::new(
2075 RestCatalogConfig::builder().uri(server.url()).build(),
2076 Some(Arc::new(LocalFsStorageFactory)),
2077 Runtime::current(),
2078 None,
2079 );
2080
2081 assert!(
2082 catalog
2083 .namespace_exists(&NamespaceIdent::new("ns1".to_string()))
2084 .await
2085 .unwrap()
2086 );
2087
2088 config_mock.assert_async().await;
2089 get_ns_mock.assert_async().await;
2090 }
2091
2092 #[tokio::test]
2093 async fn test_drop_namespace() {
2094 let mut server = Server::new_async().await;
2095
2096 let config_mock = create_config_mock(&mut server).await;
2097
2098 let drop_ns_mock = server
2099 .mock("DELETE", "/v1/namespaces/ns1")
2100 .with_status(204)
2101 .create_async()
2102 .await;
2103
2104 let catalog = RestCatalog::new(
2105 RestCatalogConfig::builder().uri(server.url()).build(),
2106 Some(Arc::new(LocalFsStorageFactory)),
2107 Runtime::current(),
2108 None,
2109 );
2110
2111 catalog
2112 .drop_namespace(&NamespaceIdent::new("ns1".to_string()))
2113 .await
2114 .unwrap();
2115
2116 config_mock.assert_async().await;
2117 drop_ns_mock.assert_async().await;
2118 }
2119
2120 #[tokio::test]
2121 async fn test_list_tables() {
2122 let mut server = Server::new_async().await;
2123
2124 let config_mock = create_config_mock(&mut server).await;
2125
2126 let list_tables_mock = server
2127 .mock("GET", "/v1/namespaces/ns1/tables")
2128 .with_status(200)
2129 .with_body(
2130 r#"{
2131 "identifiers": [
2132 {
2133 "namespace": ["ns1"],
2134 "name": "table1"
2135 },
2136 {
2137 "namespace": ["ns1"],
2138 "name": "table2"
2139 }
2140 ]
2141 }"#,
2142 )
2143 .create_async()
2144 .await;
2145
2146 let catalog = RestCatalog::new(
2147 RestCatalogConfig::builder().uri(server.url()).build(),
2148 Some(Arc::new(LocalFsStorageFactory)),
2149 Runtime::current(),
2150 None,
2151 );
2152
2153 let tables = catalog
2154 .list_tables(&NamespaceIdent::new("ns1".to_string()))
2155 .await
2156 .unwrap();
2157
2158 let expected_tables = vec![
2159 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
2160 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
2161 ];
2162
2163 assert_eq!(tables, expected_tables);
2164
2165 config_mock.assert_async().await;
2166 list_tables_mock.assert_async().await;
2167 }
2168
2169 #[tokio::test]
2170 async fn test_list_tables_with_pagination() {
2171 let mut server = Server::new_async().await;
2172
2173 let config_mock = create_config_mock(&mut server).await;
2174
2175 let list_tables_mock_page1 = server
2176 .mock("GET", "/v1/namespaces/ns1/tables")
2177 .with_status(200)
2178 .with_body(
2179 r#"{
2180 "identifiers": [
2181 {
2182 "namespace": ["ns1"],
2183 "name": "table1"
2184 },
2185 {
2186 "namespace": ["ns1"],
2187 "name": "table2"
2188 }
2189 ],
2190 "next-page-token": "token456"
2191 }"#,
2192 )
2193 .create_async()
2194 .await;
2195
2196 let list_tables_mock_page2 = server
2197 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=token456")
2198 .with_status(200)
2199 .with_body(
2200 r#"{
2201 "identifiers": [
2202 {
2203 "namespace": ["ns1"],
2204 "name": "table3"
2205 },
2206 {
2207 "namespace": ["ns1"],
2208 "name": "table4"
2209 }
2210 ]
2211 }"#,
2212 )
2213 .create_async()
2214 .await;
2215
2216 let catalog = RestCatalog::new(
2217 RestCatalogConfig::builder().uri(server.url()).build(),
2218 Some(Arc::new(LocalFsStorageFactory)),
2219 Runtime::current(),
2220 None,
2221 );
2222
2223 let tables = catalog
2224 .list_tables(&NamespaceIdent::new("ns1".to_string()))
2225 .await
2226 .unwrap();
2227
2228 let expected_tables = vec![
2229 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
2230 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
2231 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table3".to_string()),
2232 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table4".to_string()),
2233 ];
2234
2235 assert_eq!(tables, expected_tables);
2236
2237 config_mock.assert_async().await;
2238 list_tables_mock_page1.assert_async().await;
2239 list_tables_mock_page2.assert_async().await;
2240 }
2241
2242 #[tokio::test]
2243 async fn test_list_tables_with_multiple_pages() {
2244 let mut server = Server::new_async().await;
2245
2246 let config_mock = create_config_mock(&mut server).await;
2247
2248 let list_tables_mock_page1 = server
2250 .mock("GET", "/v1/namespaces/ns1/tables")
2251 .with_status(200)
2252 .with_body(
2253 r#"{
2254 "identifiers": [
2255 {
2256 "namespace": ["ns1"],
2257 "name": "table1"
2258 },
2259 {
2260 "namespace": ["ns1"],
2261 "name": "table2"
2262 }
2263 ],
2264 "next-page-token": "page2"
2265 }"#,
2266 )
2267 .create_async()
2268 .await;
2269
2270 let list_tables_mock_page2 = server
2272 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page2")
2273 .with_status(200)
2274 .with_body(
2275 r#"{
2276 "identifiers": [
2277 {
2278 "namespace": ["ns1"],
2279 "name": "table3"
2280 },
2281 {
2282 "namespace": ["ns1"],
2283 "name": "table4"
2284 }
2285 ],
2286 "next-page-token": "page3"
2287 }"#,
2288 )
2289 .create_async()
2290 .await;
2291
2292 let list_tables_mock_page3 = server
2294 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page3")
2295 .with_status(200)
2296 .with_body(
2297 r#"{
2298 "identifiers": [
2299 {
2300 "namespace": ["ns1"],
2301 "name": "table5"
2302 }
2303 ],
2304 "next-page-token": "page4"
2305 }"#,
2306 )
2307 .create_async()
2308 .await;
2309
2310 let list_tables_mock_page4 = server
2312 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page4")
2313 .with_status(200)
2314 .with_body(
2315 r#"{
2316 "identifiers": [
2317 {
2318 "namespace": ["ns1"],
2319 "name": "table6"
2320 },
2321 {
2322 "namespace": ["ns1"],
2323 "name": "table7"
2324 }
2325 ],
2326 "next-page-token": "page5"
2327 }"#,
2328 )
2329 .create_async()
2330 .await;
2331
2332 let list_tables_mock_page5 = server
2334 .mock("GET", "/v1/namespaces/ns1/tables?pageToken=page5")
2335 .with_status(200)
2336 .with_body(
2337 r#"{
2338 "identifiers": [
2339 {
2340 "namespace": ["ns1"],
2341 "name": "table8"
2342 }
2343 ]
2344 }"#,
2345 )
2346 .create_async()
2347 .await;
2348
2349 let catalog = RestCatalog::new(
2350 RestCatalogConfig::builder().uri(server.url()).build(),
2351 Some(Arc::new(LocalFsStorageFactory)),
2352 Runtime::current(),
2353 None,
2354 );
2355
2356 let tables = catalog
2357 .list_tables(&NamespaceIdent::new("ns1".to_string()))
2358 .await
2359 .unwrap();
2360
2361 let expected_tables = vec![
2362 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
2363 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
2364 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table3".to_string()),
2365 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table4".to_string()),
2366 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table5".to_string()),
2367 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table6".to_string()),
2368 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table7".to_string()),
2369 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table8".to_string()),
2370 ];
2371
2372 assert_eq!(tables, expected_tables);
2373
2374 config_mock.assert_async().await;
2376 list_tables_mock_page1.assert_async().await;
2377 list_tables_mock_page2.assert_async().await;
2378 list_tables_mock_page3.assert_async().await;
2379 list_tables_mock_page4.assert_async().await;
2380 list_tables_mock_page5.assert_async().await;
2381 }
2382
2383 #[tokio::test]
2384 async fn test_drop_tables() {
2385 let mut server = Server::new_async().await;
2386
2387 let config_mock = create_config_mock(&mut server).await;
2388
2389 let delete_table_mock = server
2390 .mock("DELETE", "/v1/namespaces/ns1/tables/table1")
2391 .with_status(204)
2392 .create_async()
2393 .await;
2394
2395 let catalog = RestCatalog::new(
2396 RestCatalogConfig::builder().uri(server.url()).build(),
2397 Some(Arc::new(LocalFsStorageFactory)),
2398 Runtime::current(),
2399 None,
2400 );
2401
2402 catalog
2403 .drop_table(&TableIdent::new(
2404 NamespaceIdent::new("ns1".to_string()),
2405 "table1".to_string(),
2406 ))
2407 .await
2408 .unwrap();
2409
2410 config_mock.assert_async().await;
2411 delete_table_mock.assert_async().await;
2412 }
2413
2414 #[tokio::test]
2415 async fn test_check_table_exists() {
2416 let mut server = Server::new_async().await;
2417
2418 let config_mock = create_config_mock_with_exists_endpoints(&mut server).await;
2419
2420 let check_table_exists_mock = server
2421 .mock("HEAD", "/v1/namespaces/ns1/tables/table1")
2422 .with_status(204)
2423 .create_async()
2424 .await;
2425
2426 let catalog = RestCatalog::new(
2427 RestCatalogConfig::builder().uri(server.url()).build(),
2428 Some(Arc::new(LocalFsStorageFactory)),
2429 Runtime::current(),
2430 None,
2431 );
2432
2433 assert!(
2434 catalog
2435 .table_exists(&TableIdent::new(
2436 NamespaceIdent::new("ns1".to_string()),
2437 "table1".to_string(),
2438 ))
2439 .await
2440 .unwrap()
2441 );
2442
2443 config_mock.assert_async().await;
2444 check_table_exists_mock.assert_async().await;
2445 }
2446
2447 #[tokio::test]
2448 async fn test_table_exists_falls_back_to_load_when_head_not_advertised() {
2449 let mut server = Server::new_async().await;
2450
2451 let config_mock = create_config_mock(&mut server).await;
2454 let load_table_mock = server
2455 .mock("GET", "/v1/namespaces/ns1/tables/table1")
2456 .with_status(200)
2457 .with_body_from_file(format!(
2458 "{}/testdata/{}",
2459 env!("CARGO_MANIFEST_DIR"),
2460 "load_table_response.json"
2461 ))
2462 .create_async()
2463 .await;
2464
2465 let catalog = RestCatalog::new(
2466 RestCatalogConfig::builder().uri(server.url()).build(),
2467 Some(Arc::new(LocalFsStorageFactory)),
2468 Runtime::current(),
2469 None,
2470 );
2471
2472 assert!(
2473 catalog
2474 .table_exists(&TableIdent::new(
2475 NamespaceIdent::new("ns1".to_string()),
2476 "table1".to_string(),
2477 ))
2478 .await
2479 .unwrap()
2480 );
2481
2482 config_mock.assert_async().await;
2483 load_table_mock.assert_async().await;
2484 }
2485
2486 #[tokio::test]
2487 async fn test_rename_table() {
2488 let mut server = Server::new_async().await;
2489
2490 let config_mock = create_config_mock(&mut server).await;
2491
2492 let rename_table_mock = server
2493 .mock("POST", "/v1/tables/rename")
2494 .with_status(204)
2495 .create_async()
2496 .await;
2497
2498 let catalog = RestCatalog::new(
2499 RestCatalogConfig::builder().uri(server.url()).build(),
2500 Some(Arc::new(LocalFsStorageFactory)),
2501 Runtime::current(),
2502 None,
2503 );
2504
2505 catalog
2506 .rename_table(
2507 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table1".to_string()),
2508 &TableIdent::new(NamespaceIdent::new("ns1".to_string()), "table2".to_string()),
2509 )
2510 .await
2511 .unwrap();
2512
2513 config_mock.assert_async().await;
2514 rename_table_mock.assert_async().await;
2515 }
2516
2517 #[tokio::test]
2518 async fn test_load_table() {
2519 let mut server = Server::new_async().await;
2520
2521 let config_mock = create_config_mock(&mut server).await;
2522
2523 let rename_table_mock = server
2524 .mock("GET", "/v1/namespaces/ns1/tables/test1")
2525 .with_status(200)
2526 .with_body_from_file(format!(
2527 "{}/testdata/{}",
2528 env!("CARGO_MANIFEST_DIR"),
2529 "load_table_response.json"
2530 ))
2531 .create_async()
2532 .await;
2533
2534 let catalog = RestCatalog::new(
2535 RestCatalogConfig::builder().uri(server.url()).build(),
2536 Some(Arc::new(LocalFsStorageFactory)),
2537 Runtime::current(),
2538 None,
2539 );
2540
2541 let table = catalog
2542 .load_table(&TableIdent::new(
2543 NamespaceIdent::new("ns1".to_string()),
2544 "test1".to_string(),
2545 ))
2546 .await
2547 .unwrap();
2548
2549 assert_eq!(
2550 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
2551 table.identifier()
2552 );
2553 assert_eq!(
2554 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
2555 table.metadata_location().unwrap()
2556 );
2557 assert_eq!(FormatVersion::V1, table.metadata().format_version());
2558 assert_eq!("s3://warehouse/database/table", table.metadata().location());
2559 assert_eq!(
2560 uuid!("b55d9dda-6561-423a-8bfc-787980ce421f"),
2561 table.metadata().uuid()
2562 );
2563 assert_eq!(
2564 Utc.timestamp_millis_opt(1646787054459).unwrap(),
2565 table.metadata().last_updated_timestamp().unwrap()
2566 );
2567 assert_eq!(
2568 vec![&Arc::new(
2569 Schema::builder()
2570 .with_fields(vec![
2571 NestedField::optional(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2572 NestedField::optional(2, "data", Type::Primitive(PrimitiveType::String))
2573 .into(),
2574 ])
2575 .build()
2576 .unwrap()
2577 )],
2578 table.metadata().schemas_iter().collect::<Vec<_>>()
2579 );
2580 assert_eq!(
2581 &HashMap::from([
2582 ("owner".to_string(), "bryan".to_string()),
2583 (
2584 "write.metadata.compression-codec".to_string(),
2585 "gzip".to_string()
2586 )
2587 ]),
2588 table.metadata().properties()
2589 );
2590 assert_eq!(vec![&Arc::new(Snapshot::builder()
2591 .with_snapshot_id(3497810964824022504)
2592 .with_timestamp_ms(1646787054459)
2593 .with_manifest_list("s3://warehouse/database/table/metadata/snap-3497810964824022504-1-c4f68204-666b-4e50-a9df-b10c34bf6b82.avro")
2594 .with_sequence_number(0)
2595 .with_schema_id(0)
2596 .with_summary(Summary {
2597 operation: Operation::Append,
2598 additional_properties: HashMap::from_iter([
2599 ("spark.app.id", "local-1646787004168"),
2600 ("added-data-files", "1"),
2601 ("added-records", "1"),
2602 ("added-files-size", "697"),
2603 ("changed-partition-count", "1"),
2604 ("total-records", "1"),
2605 ("total-files-size", "697"),
2606 ("total-data-files", "1"),
2607 ("total-delete-files", "0"),
2608 ("total-position-deletes", "0"),
2609 ("total-equality-deletes", "0")
2610 ].iter().map(|p| (p.0.to_string(), p.1.to_string()))),
2611 }).build()
2612 )], table.metadata().snapshots().collect::<Vec<_>>());
2613 assert_eq!(
2614 &[SnapshotLog {
2615 timestamp_ms: 1646787054459,
2616 snapshot_id: 3497810964824022504,
2617 }],
2618 table.metadata().history()
2619 );
2620 assert_eq!(
2621 vec![&Arc::new(SortOrder {
2622 order_id: 0,
2623 fields: vec![],
2624 })],
2625 table.metadata().sort_orders_iter().collect::<Vec<_>>()
2626 );
2627
2628 config_mock.assert_async().await;
2629 rename_table_mock.assert_async().await;
2630 }
2631
2632 #[tokio::test]
2633 async fn test_load_table_404() {
2634 let mut server = Server::new_async().await;
2635
2636 let config_mock = create_config_mock(&mut server).await;
2637
2638 let rename_table_mock = server
2639 .mock("GET", "/v1/namespaces/ns1/tables/test1")
2640 .with_status(404)
2641 .with_body(r#"
2642{
2643 "error": {
2644 "message": "Table does not exist: ns1.test1 in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
2645 "type": "NoSuchNamespaceErrorException",
2646 "code": 404
2647 }
2648}
2649 "#)
2650 .create_async()
2651 .await;
2652
2653 let catalog = RestCatalog::new(
2654 RestCatalogConfig::builder().uri(server.url()).build(),
2655 Some(Arc::new(LocalFsStorageFactory)),
2656 Runtime::current(),
2657 None,
2658 );
2659
2660 let table = catalog
2661 .load_table(&TableIdent::new(
2662 NamespaceIdent::new("ns1".to_string()),
2663 "test1".to_string(),
2664 ))
2665 .await;
2666
2667 assert!(table.is_err());
2668 assert!(table.err().unwrap().message().contains("does not exist"));
2669
2670 config_mock.assert_async().await;
2671 rename_table_mock.assert_async().await;
2672 }
2673
2674 #[tokio::test]
2675 async fn test_create_table() {
2676 let mut server = Server::new_async().await;
2677
2678 let config_mock = create_config_mock(&mut server).await;
2679
2680 let create_table_mock = server
2681 .mock("POST", "/v1/namespaces/ns1/tables")
2682 .with_status(200)
2683 .with_body_from_file(format!(
2684 "{}/testdata/{}",
2685 env!("CARGO_MANIFEST_DIR"),
2686 "create_table_response.json"
2687 ))
2688 .create_async()
2689 .await;
2690
2691 let catalog = RestCatalog::new(
2692 RestCatalogConfig::builder().uri(server.url()).build(),
2693 Some(Arc::new(LocalFsStorageFactory)),
2694 Runtime::current(),
2695 None,
2696 );
2697
2698 let table_creation = TableCreation::builder()
2699 .name("test1".to_string())
2700 .schema(
2701 Schema::builder()
2702 .with_fields(vec![
2703 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
2704 .into(),
2705 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
2706 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
2707 .into(),
2708 ])
2709 .with_schema_id(1)
2710 .with_identifier_field_ids(vec![2])
2711 .build()
2712 .unwrap(),
2713 )
2714 .properties(HashMap::from([("owner".to_string(), "testx".to_string())]))
2715 .partition_spec(
2716 UnboundPartitionSpec::builder()
2717 .add_partition_fields(vec![
2718 UnboundPartitionField::builder()
2719 .source_id(1)
2720 .transform(Transform::Truncate(3))
2721 .name("id".to_string())
2722 .build(),
2723 ])
2724 .unwrap()
2725 .build(),
2726 )
2727 .sort_order(
2728 SortOrder::builder()
2729 .with_sort_field(
2730 SortField::builder()
2731 .source_id(2)
2732 .transform(Transform::Identity)
2733 .direction(SortDirection::Ascending)
2734 .null_order(NullOrder::First)
2735 .build(),
2736 )
2737 .build_unbound()
2738 .unwrap(),
2739 )
2740 .build();
2741
2742 let table = catalog
2743 .create_table(&NamespaceIdent::from_strs(["ns1"]).unwrap(), table_creation)
2744 .await
2745 .unwrap();
2746
2747 assert_eq!(
2748 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
2749 table.identifier()
2750 );
2751 assert_eq!(
2752 "s3://warehouse/database/table/metadata.json",
2753 table.metadata_location().unwrap()
2754 );
2755 assert_eq!(FormatVersion::V1, table.metadata().format_version());
2756 assert_eq!("s3://warehouse/database/table", table.metadata().location());
2757 assert_eq!(
2758 uuid!("bf289591-dcc0-4234-ad4f-5c3eed811a29"),
2759 table.metadata().uuid()
2760 );
2761 assert_eq!(
2762 1657810967051,
2763 table
2764 .metadata()
2765 .last_updated_timestamp()
2766 .unwrap()
2767 .timestamp_millis()
2768 );
2769 assert_eq!(
2770 vec![&Arc::new(
2771 Schema::builder()
2772 .with_fields(vec![
2773 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
2774 .into(),
2775 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
2776 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
2777 .into(),
2778 ])
2779 .with_schema_id(0)
2780 .with_identifier_field_ids(vec![2])
2781 .build()
2782 .unwrap()
2783 )],
2784 table.metadata().schemas_iter().collect::<Vec<_>>()
2785 );
2786 assert_eq!(
2787 &HashMap::from([
2788 (
2789 "write.delete.parquet.compression-codec".to_string(),
2790 "zstd".to_string()
2791 ),
2792 (
2793 "write.metadata.compression-codec".to_string(),
2794 "gzip".to_string()
2795 ),
2796 (
2797 "write.summary.partition-limit".to_string(),
2798 "100".to_string()
2799 ),
2800 (
2801 "write.parquet.compression-codec".to_string(),
2802 "zstd".to_string()
2803 ),
2804 ]),
2805 table.metadata().properties()
2806 );
2807 assert!(table.metadata().current_snapshot().is_none());
2808 assert!(table.metadata().history().is_empty());
2809 assert_eq!(
2810 vec![&Arc::new(SortOrder {
2811 order_id: 0,
2812 fields: vec![],
2813 })],
2814 table.metadata().sort_orders_iter().collect::<Vec<_>>()
2815 );
2816
2817 config_mock.assert_async().await;
2818 create_table_mock.assert_async().await;
2819 }
2820
2821 #[tokio::test]
2822 async fn test_create_table_409() {
2823 let mut server = Server::new_async().await;
2824
2825 let config_mock = create_config_mock(&mut server).await;
2826
2827 let create_table_mock = server
2828 .mock("POST", "/v1/namespaces/ns1/tables")
2829 .with_status(409)
2830 .with_body(r#"
2831{
2832 "error": {
2833 "message": "Table already exists: ns1.test1 in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e",
2834 "type": "AlreadyExistsException",
2835 "code": 409
2836 }
2837}
2838 "#)
2839 .create_async()
2840 .await;
2841
2842 let catalog = RestCatalog::new(
2843 RestCatalogConfig::builder().uri(server.url()).build(),
2844 Some(Arc::new(LocalFsStorageFactory)),
2845 Runtime::current(),
2846 None,
2847 );
2848
2849 let table_creation = TableCreation::builder()
2850 .name("test1".to_string())
2851 .schema(
2852 Schema::builder()
2853 .with_fields(vec![
2854 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
2855 .into(),
2856 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
2857 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
2858 .into(),
2859 ])
2860 .with_schema_id(1)
2861 .with_identifier_field_ids(vec![2])
2862 .build()
2863 .unwrap(),
2864 )
2865 .properties(HashMap::from([("owner".to_string(), "testx".to_string())]))
2866 .build();
2867
2868 let table_result = catalog
2869 .create_table(&NamespaceIdent::from_strs(["ns1"]).unwrap(), table_creation)
2870 .await;
2871
2872 assert!(table_result.is_err());
2873 assert!(
2874 table_result
2875 .err()
2876 .unwrap()
2877 .message()
2878 .contains("already exists")
2879 );
2880
2881 config_mock.assert_async().await;
2882 create_table_mock.assert_async().await;
2883 }
2884
2885 #[tokio::test]
2886 async fn test_update_table() {
2887 let mut server = Server::new_async().await;
2888
2889 let config_mock = create_config_mock(&mut server).await;
2890
2891 let load_table_mock = server
2892 .mock("GET", "/v1/namespaces/ns1/tables/test1")
2893 .with_status(200)
2894 .with_body_from_file(format!(
2895 "{}/testdata/{}",
2896 env!("CARGO_MANIFEST_DIR"),
2897 "load_table_response.json"
2898 ))
2899 .create_async()
2900 .await;
2901
2902 let update_table_mock = server
2903 .mock("POST", "/v1/namespaces/ns1/tables/test1")
2904 .with_status(200)
2905 .with_body_from_file(format!(
2906 "{}/testdata/{}",
2907 env!("CARGO_MANIFEST_DIR"),
2908 "update_table_response.json"
2909 ))
2910 .create_async()
2911 .await;
2912
2913 let catalog = RestCatalog::new(
2914 RestCatalogConfig::builder().uri(server.url()).build(),
2915 Some(Arc::new(LocalFsStorageFactory)),
2916 Runtime::current(),
2917 None,
2918 );
2919
2920 let table1 = {
2921 let file = File::open(format!(
2922 "{}/testdata/{}",
2923 env!("CARGO_MANIFEST_DIR"),
2924 "create_table_response.json"
2925 ))
2926 .unwrap();
2927 let reader = BufReader::new(file);
2928 let resp = serde_json::from_reader::<_, LoadTableResult>(reader).unwrap();
2929
2930 Table::builder()
2931 .metadata(resp.metadata)
2932 .metadata_location(resp.metadata_location.unwrap())
2933 .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
2934 .file_io(FileIO::new_with_fs())
2935 .runtime(test_runtime())
2936 .build()
2937 .unwrap()
2938 };
2939
2940 let tx = Transaction::new(&table1);
2941 let table = tx
2942 .upgrade_table_version()
2943 .set_format_version(FormatVersion::V2)
2944 .apply(tx)
2945 .unwrap()
2946 .commit(&catalog)
2947 .await
2948 .unwrap();
2949
2950 assert_eq!(
2951 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
2952 table.identifier()
2953 );
2954 assert_eq!(
2955 "s3://warehouse/database/table/metadata.json",
2956 table.metadata_location().unwrap()
2957 );
2958 assert_eq!(FormatVersion::V2, table.metadata().format_version());
2959 assert_eq!("s3://warehouse/database/table", table.metadata().location());
2960 assert_eq!(
2961 uuid!("bf289591-dcc0-4234-ad4f-5c3eed811a29"),
2962 table.metadata().uuid()
2963 );
2964 assert_eq!(
2965 1657810967051,
2966 table
2967 .metadata()
2968 .last_updated_timestamp()
2969 .unwrap()
2970 .timestamp_millis()
2971 );
2972 assert_eq!(
2973 vec![&Arc::new(
2974 Schema::builder()
2975 .with_fields(vec![
2976 NestedField::optional(1, "foo", Type::Primitive(PrimitiveType::String))
2977 .into(),
2978 NestedField::required(2, "bar", Type::Primitive(PrimitiveType::Int)).into(),
2979 NestedField::optional(3, "baz", Type::Primitive(PrimitiveType::Boolean))
2980 .into(),
2981 ])
2982 .with_schema_id(0)
2983 .with_identifier_field_ids(vec![2])
2984 .build()
2985 .unwrap()
2986 )],
2987 table.metadata().schemas_iter().collect::<Vec<_>>()
2988 );
2989 assert_eq!(
2990 &HashMap::from([
2991 (
2992 "write.delete.parquet.compression-codec".to_string(),
2993 "zstd".to_string()
2994 ),
2995 (
2996 "write.metadata.compression-codec".to_string(),
2997 "gzip".to_string()
2998 ),
2999 (
3000 "write.summary.partition-limit".to_string(),
3001 "100".to_string()
3002 ),
3003 (
3004 "write.parquet.compression-codec".to_string(),
3005 "zstd".to_string()
3006 ),
3007 ]),
3008 table.metadata().properties()
3009 );
3010 assert!(table.metadata().current_snapshot().is_none());
3011 assert!(table.metadata().history().is_empty());
3012 assert_eq!(
3013 vec![&Arc::new(SortOrder {
3014 order_id: 0,
3015 fields: vec![],
3016 })],
3017 table.metadata().sort_orders_iter().collect::<Vec<_>>()
3018 );
3019
3020 config_mock.assert_async().await;
3021 update_table_mock.assert_async().await;
3022 load_table_mock.assert_async().await
3023 }
3024
3025 #[tokio::test]
3026 async fn test_update_table_404() {
3027 let mut server = Server::new_async().await;
3028
3029 let config_mock = create_config_mock(&mut server).await;
3030
3031 let load_table_mock = server
3032 .mock("GET", "/v1/namespaces/ns1/tables/test1")
3033 .with_status(200)
3034 .with_body_from_file(format!(
3035 "{}/testdata/{}",
3036 env!("CARGO_MANIFEST_DIR"),
3037 "load_table_response.json"
3038 ))
3039 .create_async()
3040 .await;
3041
3042 let update_table_mock = server
3043 .mock("POST", "/v1/namespaces/ns1/tables/test1")
3044 .with_status(404)
3045 .with_body(
3046 r#"
3047{
3048 "error": {
3049 "message": "The given table does not exist",
3050 "type": "NoSuchTableException",
3051 "code": 404
3052 }
3053}
3054 "#,
3055 )
3056 .create_async()
3057 .await;
3058
3059 let catalog = RestCatalog::new(
3060 RestCatalogConfig::builder().uri(server.url()).build(),
3061 Some(Arc::new(LocalFsStorageFactory)),
3062 Runtime::current(),
3063 None,
3064 );
3065
3066 let table1 = {
3067 let file = File::open(format!(
3068 "{}/testdata/{}",
3069 env!("CARGO_MANIFEST_DIR"),
3070 "create_table_response.json"
3071 ))
3072 .unwrap();
3073 let reader = BufReader::new(file);
3074 let resp = serde_json::from_reader::<_, LoadTableResult>(reader).unwrap();
3075
3076 Table::builder()
3077 .metadata(resp.metadata)
3078 .metadata_location(resp.metadata_location.unwrap())
3079 .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
3080 .file_io(FileIO::new_with_fs())
3081 .runtime(test_runtime())
3082 .build()
3083 .unwrap()
3084 };
3085
3086 let tx = Transaction::new(&table1);
3087 let table_result = tx
3088 .upgrade_table_version()
3089 .set_format_version(FormatVersion::V2)
3090 .apply(tx)
3091 .unwrap()
3092 .commit(&catalog)
3093 .await;
3094
3095 assert!(table_result.is_err());
3096 assert!(
3097 table_result
3098 .err()
3099 .unwrap()
3100 .message()
3101 .contains("does not exist")
3102 );
3103
3104 config_mock.assert_async().await;
3105 update_table_mock.assert_async().await;
3106 load_table_mock.assert_async().await;
3107 }
3108
3109 #[tokio::test]
3110 async fn test_register_table() {
3111 let mut server = Server::new_async().await;
3112
3113 let config_mock = create_config_mock(&mut server).await;
3114
3115 let register_table_mock = server
3116 .mock("POST", "/v1/namespaces/ns1/register")
3117 .with_status(200)
3118 .with_body_from_file(format!(
3119 "{}/testdata/{}",
3120 env!("CARGO_MANIFEST_DIR"),
3121 "load_table_response.json"
3122 ))
3123 .create_async()
3124 .await;
3125
3126 let catalog = RestCatalog::new(
3127 RestCatalogConfig::builder().uri(server.url()).build(),
3128 Some(Arc::new(LocalFsStorageFactory)),
3129 Runtime::current(),
3130 None,
3131 );
3132 let table_ident =
3133 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "test1".to_string());
3134 let metadata_location = String::from(
3135 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
3136 );
3137
3138 let table = catalog
3139 .register_table(&table_ident, metadata_location)
3140 .await
3141 .unwrap();
3142
3143 assert_eq!(
3144 &TableIdent::from_strs(vec!["ns1", "test1"]).unwrap(),
3145 table.identifier()
3146 );
3147 assert_eq!(
3148 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
3149 table.metadata_location().unwrap()
3150 );
3151
3152 config_mock.assert_async().await;
3153 register_table_mock.assert_async().await;
3154 }
3155
3156 #[tokio::test]
3157 async fn test_register_table_404() {
3158 let mut server = Server::new_async().await;
3159
3160 let config_mock = create_config_mock(&mut server).await;
3161
3162 let register_table_mock = server
3163 .mock("POST", "/v1/namespaces/ns1/register")
3164 .with_status(404)
3165 .with_body(
3166 r#"
3167{
3168 "error": {
3169 "message": "The namespace specified does not exist",
3170 "type": "NoSuchNamespaceErrorException",
3171 "code": 404
3172 }
3173}
3174 "#,
3175 )
3176 .create_async()
3177 .await;
3178
3179 let catalog = RestCatalog::new(
3180 RestCatalogConfig::builder().uri(server.url()).build(),
3181 Some(Arc::new(LocalFsStorageFactory)),
3182 Runtime::current(),
3183 None,
3184 );
3185
3186 let table_ident =
3187 TableIdent::new(NamespaceIdent::new("ns1".to_string()), "test1".to_string());
3188 let metadata_location = String::from(
3189 "s3://warehouse/database/table/metadata/00001-5f2f8166-244c-4eae-ac36-384ecdec81fc.gz.metadata.json",
3190 );
3191 let table = catalog
3192 .register_table(&table_ident, metadata_location)
3193 .await;
3194
3195 assert!(table.is_err());
3196 assert!(table.err().unwrap().message().contains("does not exist"));
3197
3198 config_mock.assert_async().await;
3199 register_table_mock.assert_async().await;
3200 }
3201
3202 #[tokio::test]
3203 async fn test_create_rest_catalog() {
3204 let builder = RestCatalogBuilder::default().with_client(Client::new());
3205
3206 let catalog = builder
3207 .load(
3208 "test",
3209 HashMap::from([
3210 (
3211 REST_CATALOG_PROP_URI.to_string(),
3212 "http://localhost:8080".to_string(),
3213 ),
3214 ("a".to_string(), "b".to_string()),
3215 ]),
3216 )
3217 .await;
3218
3219 assert!(catalog.is_ok());
3220
3221 let catalog_config = catalog.unwrap().user_config;
3222 assert_eq!(catalog_config.name.as_deref(), Some("test"));
3223 assert_eq!(catalog_config.uri, "http://localhost:8080");
3224 assert_eq!(catalog_config.warehouse, None);
3225 assert!(catalog_config.client.is_some());
3226
3227 assert_eq!(catalog_config.props.get("a"), Some(&"b".to_string()));
3228 assert!(!catalog_config.props.contains_key(REST_CATALOG_PROP_URI));
3229 }
3230
3231 #[tokio::test]
3232 async fn test_create_rest_catalog_no_uri() {
3233 let builder = RestCatalogBuilder::default();
3234
3235 let catalog = builder
3236 .load(
3237 "test",
3238 HashMap::from([(
3239 REST_CATALOG_PROP_WAREHOUSE.to_string(),
3240 "s3://warehouse".to_string(),
3241 )]),
3242 )
3243 .await;
3244
3245 assert!(catalog.is_err());
3246 if let Err(err) = catalog {
3247 assert_eq!(err.kind(), ErrorKind::DataInvalid);
3248 assert_eq!(err.message(), "Catalog uri is required");
3249 }
3250 }
3251}