Skip to main content

iceberg_catalog_rest/
catalog.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! This module contains the iceberg REST catalog implementation.
19
20use 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
51/// REST catalog URI
52pub const REST_CATALOG_PROP_URI: &str = "uri";
53/// REST catalog warehouse location
54pub const REST_CATALOG_PROP_WAREHOUSE: &str = "warehouse";
55/// Disable header redaction in error logs (defaults to false for security)
56pub 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/// Builder for [`RestCatalog`].
63#[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        // Collect other remaining properties
125        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    /// Configures the catalog with a custom HTTP client.
160    pub fn with_client(mut self, client: Client) -> Self {
161        self.config.client = Some(client);
162        self
163    }
164}
165
166/// Rest catalog configuration.
167#[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    /// Get the client from the config.
235    pub(crate) fn client(&self) -> Option<Client> {
236        self.client.clone()
237    }
238
239    /// Get the token from the config.
240    ///
241    /// The client can use this token to send requests.
242    pub(crate) fn token(&self) -> Option<String> {
243        self.props.get("token").cloned()
244    }
245
246    /// Get the credentials from the config. The client can use these credentials to fetch a new
247    /// token.
248    ///
249    /// ## Output
250    ///
251    /// - `None`: No credential is set.
252    /// - `Some(None, client_secret)`: No client_id is set, use client_secret directly.
253    /// - `Some(Some(client_id), client_secret)`: Both client_id and client_secret are set.
254    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    /// Get the extra headers from config, which includes:
266    ///
267    /// - `content-type`
268    /// - `x-client-version`
269    /// - `user-agent`
270    /// - All headers specified by `header.xxx` in props.
271    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    /// Get the optional OAuth headers from the config.
314    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    /// Check if header redaction is disabled in error logs.
334    ///
335    /// Returns true if the `disable-header-redaction` property is set to "true".
336    /// Defaults to false for security (headers are redacted by default).
337    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    /// Merge the `RestCatalogConfig` with the a [`CatalogConfig`] (fetched from the REST server).
345    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    /// Runtime config is fetched from rest server and stored here.
363    ///
364    /// It's could be different from the user config.
365    config: RestCatalogConfig,
366    /// Capabilities the server advertises (see [`RestCatalog::supports_endpoint`]).
367    endpoints: HashSet<Endpoint>,
368}
369
370/// Rest catalog implementation.
371#[derive(Debug)]
372pub struct RestCatalog {
373    /// User config is stored as-is and never be changed.
374    ///
375    /// It could be different from the config fetched from the server and used at runtime.
376    user_config: RestCatalogConfig,
377    ctx: OnceCell<RestContext>,
378    /// Storage factory for creating FileIO instances.
379    storage_factory: Option<Arc<dyn StorageFactory>>,
380    runtime: Runtime,
381    /// Optional KMS client for encrypted tables.
382    kms_client: Option<Arc<dyn KeyManagementClient>>,
383}
384
385impl RestCatalog {
386    /// Creates a `RestCatalog` from a [`RestCatalogConfig`].
387    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    /// Sends a DELETE request for the given table, optionally requesting purge.
403    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    /// Gets the [`RestContext`] from the catalog.
432    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                // Use the advertised endpoints as-is, falling back to
438                // `DEFAULT_ENDPOINTS` when absent or empty.
439                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    /// Returns whether the server supports `endpoint`, per the `endpoints` it
458    /// advertised in `GET /v1/config` (or a default base set when it advertised
459    /// none).
460    pub(crate) async fn supports_endpoint(&self, endpoint: &Endpoint) -> Result<bool> {
461        Ok(self.context().await?.endpoints.contains(endpoint))
462    }
463
464    /// Issue a `HEAD` request to `url` and interpret it as an existence check:
465    /// `2xx` means it exists, `404` means it doesn't.
466    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    /// Load the runtime config from the server by `user_config`.
482    ///
483    /// It's required for a REST catalog to update its config after creation.
484    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        // If the warehouse is a logical identifier instead of a URL we don't want
519        // to raise an exception
520        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        // Require a StorageFactory to be provided
534        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    /// Invalidate the current token without generating a new one. On the next request, the client
550    /// will attempt to generate a new token.
551    pub async fn invalidate_token(&self) -> Result<()> {
552        self.context().await?.client.invalidate_token().await
553    }
554
555    /// Invalidate the current token and set a new one. Generates a new token before invalidating
556    /// the current token, meaning the old token will be used until this function acquires the lock
557    /// and overwrites the token.
558    ///
559    /// If credential is invalid, or the request fails, this method will return an error and leave
560    /// the current token unchanged.
561    pub async fn regenerate_token(&self) -> Result<()> {
562        self.context().await?.client.regenerate_token().await
563    }
564}
565
566/// All requests and expected responses are derived from the REST catalog API spec:
567/// https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml
568#[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            // Filter on `parent={namespace}` if a parent namespace exists.
583            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        // Prefer a cheap HEAD when the server advertises it; otherwise fall back
691        // to loading the namespace (GET) and treating a missing namespace as
692        // `false`, so this still works against servers that don't advertise the
693        // HEAD route.
694        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    /// Create a new table inside the namespace.
789    ///
790    /// In the resulting table, if there are any config properties that
791    /// are present in both the response from the REST server and the
792    /// config provided when creating this `RestCatalog` instance then
793    /// the value provided locally to the `RestCatalog` will take precedence.
794    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    /// Load table from the catalog.
876    ///
877    /// If there are any config properties that are present in both the response from the REST
878    /// server and the config provided when creating this `RestCatalog` instance, then the value
879    /// provided locally to the `RestCatalog` will take precedence.
880    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    /// Drop a table from the catalog.
936    async fn drop_table(&self, table: &TableIdent) -> Result<()> {
937        self.delete_table(table, false).await
938    }
939
940    /// Drop a table from the catalog and purge its data by sending
941    /// `purgeRequested=true` to the REST server.
942    async fn purge_table(&self, table: &TableIdent) -> Result<()> {
943        self.delete_table(table, true).await
944    }
945
946    /// Check if a table exists in the catalog.
947    async fn table_exists(&self, table: &TableIdent) -> Result<bool> {
948        // Prefer a cheap HEAD when the server advertises it; otherwise fall back
949        // to loading the table (GET) and treating a missing table as `false`, so
950        // this still works against servers that don't advertise the HEAD route.
951        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    /// Rename a table in the catalog.
965    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    /// Config mock that advertises the HEAD table/namespace-exists endpoints, so
1219    /// `{table,namespace}_exists` take the HEAD path rather than the GET fallback.
1220    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        // Advertised list is present but does not include this route.
1270        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        // A server that omits the `endpoints` field is assumed to support the
1297        // standard base operations.
1298        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        // But not an optional endpoint that must be advertised.
1303        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        // An explicit empty list is treated the same as an absent field: fall
1316        // back to the standard base set.
1317        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        // original token is left intact
1573        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        // Page 1
1831        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        // Page 2
1846        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        // Page 3
1861        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        // Page 4
1875        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        // Page 5 (final page)
1890        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        // Verify all page requests were made
1930        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        // No `endpoints` advertised, and the default set has no HEAD namespace
2060        // route, so `namespace_exists` falls back to a GET load-namespace.
2061        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        // Page 1
2249        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        // Page 2
2271        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        // Page 3
2293        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        // Page 4
2311        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        // Page 5 (final page)
2333        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        // Verify all page requests were made
2375        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        // No `endpoints` advertised, and the default set has no HEAD table
2452        // route, so `table_exists` falls back to a GET load-table.
2453        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}