iceberg_catalog_rest/
types.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//! Request and response types for the Iceberg REST API.
19
20use std::collections::HashMap;
21
22use iceberg::spec::{Schema, SortOrder, TableMetadata, UnboundPartitionSpec};
23use iceberg::{
24    Error, ErrorKind, Namespace, NamespaceIdent, TableIdent, TableRequirement, TableUpdate,
25};
26use serde_derive::{Deserialize, Serialize};
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub(super) struct CatalogConfig {
30    pub(super) overrides: HashMap<String, String>,
31    pub(super) defaults: HashMap<String, String>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35/// Wrapper for all non-2xx error responses from the REST API
36pub struct ErrorResponse {
37    error: ErrorModel,
38}
39
40impl From<ErrorResponse> for Error {
41    fn from(resp: ErrorResponse) -> Error {
42        resp.error.into()
43    }
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47/// Error payload returned in a response with further details on the error
48pub struct ErrorModel {
49    /// Human-readable error message
50    pub message: String,
51    /// Internal type definition of the error
52    pub r#type: String,
53    /// HTTP response code
54    pub code: u16,
55    /// Optional error stack / context
56    pub stack: Option<Vec<String>>,
57}
58
59impl From<ErrorModel> for Error {
60    fn from(value: ErrorModel) -> Self {
61        let mut error = Error::new(ErrorKind::DataInvalid, value.message)
62            .with_context("type", value.r#type)
63            .with_context("code", format!("{}", value.code));
64
65        if let Some(stack) = value.stack {
66            error = error.with_context("stack", stack.join("\n"));
67        }
68
69        error
70    }
71}
72
73#[derive(Debug, Serialize, Deserialize)]
74pub(super) struct OAuthError {
75    pub(super) error: String,
76    pub(super) error_description: Option<String>,
77    pub(super) error_uri: Option<String>,
78}
79
80impl From<OAuthError> for Error {
81    fn from(value: OAuthError) -> Self {
82        let mut error = Error::new(
83            ErrorKind::DataInvalid,
84            format!("OAuthError: {}", value.error),
85        );
86
87        if let Some(desc) = value.error_description {
88            error = error.with_context("description", desc);
89        }
90
91        if let Some(uri) = value.error_uri {
92            error = error.with_context("uri", uri);
93        }
94
95        error
96    }
97}
98
99#[derive(Debug, Serialize, Deserialize)]
100pub(super) struct TokenResponse {
101    pub(super) access_token: String,
102    pub(super) token_type: String,
103    pub(super) expires_in: Option<u64>,
104    pub(super) issued_token_type: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108/// Namespace response
109pub struct NamespaceResponse {
110    /// Namespace identifier
111    pub namespace: NamespaceIdent,
112    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
113    /// Properties stored on the namespace, if supported by the server.
114    pub properties: HashMap<String, String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118/// Create namespace request
119pub struct CreateNamespaceRequest {
120    /// Name of the namespace to create
121    pub namespace: NamespaceIdent,
122    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
123    /// Properties to set on the namespace
124    pub properties: HashMap<String, String>,
125}
126
127impl From<&Namespace> for NamespaceResponse {
128    fn from(value: &Namespace) -> Self {
129        Self {
130            namespace: value.name().clone(),
131            properties: value.properties().clone(),
132        }
133    }
134}
135
136impl From<NamespaceResponse> for Namespace {
137    fn from(value: NamespaceResponse) -> Self {
138        Namespace::with_properties(value.namespace, value.properties)
139    }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
143#[serde(rename_all = "kebab-case")]
144/// Response containing a list of namespace identifiers, with optional pagination support.
145pub struct ListNamespaceResponse {
146    /// List of namespace identifiers returned by the server
147    pub namespaces: Vec<NamespaceIdent>,
148    /// Opaque token for pagination. If present, indicates there are more results available.
149    /// Use this value in subsequent requests to retrieve the next page.
150    pub next_page_token: Option<String>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154/// Request to update properties on a namespace.
155///
156/// Properties that are not in the request are not modified or removed by this call.
157/// Server implementations are not required to support namespace properties.
158pub struct UpdateNamespacePropertiesRequest {
159    /// List of property keys to remove from the namespace
160    pub removals: Option<Vec<String>>,
161    /// Map of property keys to values to set or update on the namespace
162    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
163    pub updates: HashMap<String, String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
167/// Response from updating namespace properties, indicating which properties were changed.
168pub struct UpdateNamespacePropertiesResponse {
169    /// List of property keys that were added or updated
170    pub updated: Vec<String>,
171    /// List of properties that were removed
172    pub removed: Vec<String>,
173    /// List of properties requested for removal that were not found in the namespace's properties.
174    /// Represents a partial success response. Servers do not need to implement this.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub missing: Option<Vec<String>>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180#[serde(rename_all = "kebab-case")]
181/// Response containing a list of table identifiers, with optional pagination support.
182pub struct ListTablesResponse {
183    /// List of table identifiers under the requested namespace
184    pub identifiers: Vec<TableIdent>,
185    /// Opaque token for pagination. If present, indicates there are more results available.
186    /// Use this value in subsequent requests to retrieve the next page.
187    #[serde(default)]
188    pub next_page_token: Option<String>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192/// Request to rename a table from one identifier to another.
193///
194/// It's valid to move a table across namespaces, but the server implementation
195/// is not required to support it.
196pub struct RenameTableRequest {
197    /// Current table identifier to rename
198    pub source: TableIdent,
199    /// New table identifier to rename to
200    pub destination: TableIdent,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "kebab-case")]
205/// Result returned when a table is successfully loaded or created.
206///
207/// The table metadata JSON is returned in the `metadata` field. The corresponding file location
208/// of table metadata should be returned in the `metadata_location` field, unless the metadata
209/// is not yet committed. For example, a create transaction may return metadata that is staged
210/// but not committed.
211///
212/// The `config` map returns table-specific configuration for the table's resources, including
213/// its HTTP client and FileIO. For example, config may contain a specific FileIO implementation
214/// class for the table depending on its underlying storage.
215pub struct LoadTableResult {
216    /// May be null if the table is staged as part of a transaction
217    pub metadata_location: Option<String>,
218    /// The table's full metadata
219    pub metadata: TableMetadata,
220    /// Table-specific configuration overriding catalog configuration
221    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
222    pub config: HashMap<String, String>,
223    /// Storage credentials for accessing table data. Clients should check this field
224    /// before falling back to credentials in the `config` field.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub storage_credentials: Option<Vec<StorageCredential>>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
230/// Storage credential for a specific location prefix.
231///
232/// Indicates a storage location prefix where the credential is relevant. Clients should
233/// choose the most specific prefix (by selecting the longest prefix) if several credentials
234/// of the same type are available.
235pub struct StorageCredential {
236    /// Storage location prefix where this credential is relevant
237    pub prefix: String,
238    /// Configuration map containing credential information
239    pub config: HashMap<String, String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
243#[serde(rename_all = "kebab-case")]
244/// Request to create a new table in a namespace.
245///
246/// If `stage_create` is false, the table is created immediately.
247/// If `stage_create` is true, the table is not created, but table metadata is initialized
248/// and returned. The service should prepare as needed for a commit to the table commit
249/// endpoint to complete the create transaction.
250pub struct CreateTableRequest {
251    /// Name of the table to create
252    pub name: String,
253    /// Optional table location. If not provided, the server will choose a location.
254    pub location: Option<String>,
255    /// Table schema
256    pub schema: Schema,
257    /// Optional partition specification. If not provided, the table will be unpartitioned.
258    pub partition_spec: Option<UnboundPartitionSpec>,
259    /// Optional sort order for the table
260    pub write_order: Option<SortOrder>,
261    /// Whether to stage the create for a transaction (true) or create immediately (false)
262    pub stage_create: Option<bool>,
263    /// Optional properties to set on the table
264    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
265    pub properties: HashMap<String, String>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
269/// Request to commit updates to a table.
270///
271/// Commits have two parts: requirements and updates. Requirements are assertions that will
272/// be validated before attempting to make and commit changes. Updates are changes to make
273/// to table metadata.
274///
275/// Create table transactions that are started by createTable with `stage-create` set to true
276/// are committed using this request. Transactions should include all changes to the table,
277/// including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate.
278pub struct CommitTableRequest {
279    /// Table identifier to update; must be present for CommitTransactionRequest
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub identifier: Option<TableIdent>,
282    /// List of requirements that must be satisfied before committing changes
283    pub requirements: Vec<TableRequirement>,
284    /// List of updates to apply to the table metadata
285    pub updates: Vec<TableUpdate>,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289#[serde(rename_all = "kebab-case")]
290/// Response returned when a table is successfully updated.
291///
292/// The table metadata JSON is returned in the metadata field. The corresponding file location
293/// of table metadata must be returned in the metadata-location field. Clients can check whether
294/// metadata has changed by comparing metadata locations.
295pub struct CommitTableResponse {
296    /// Location of the updated table metadata file
297    pub metadata_location: String,
298    /// The table's updated metadata
299    pub metadata: TableMetadata,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
303#[serde(rename_all = "kebab-case")]
304/// Request to register a table using an existing metadata file location.
305pub struct RegisterTableRequest {
306    /// Name of the table to register
307    pub name: String,
308    /// Location of the metadata file for the table
309    pub metadata_location: String,
310    /// Whether to overwrite table metadata if the table already exists
311    pub overwrite: Option<bool>,
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_namespace_response_serde() {
320        let json = serde_json::json!({
321            "namespace": ["nested", "ns"],
322            "properties": {
323                "key1": "value1",
324                "key2": "value2"
325            }
326        });
327        let ns_response: NamespaceResponse =
328            serde_json::from_value(json.clone()).expect("Deserialization failed");
329        assert_eq!(ns_response, NamespaceResponse {
330            namespace: NamespaceIdent::from_vec(vec!["nested".to_string(), "ns".to_string()])
331                .unwrap(),
332            properties: HashMap::from([
333                ("key1".to_string(), "value1".to_string()),
334                ("key2".to_string(), "value2".to_string()),
335            ]),
336        });
337        assert_eq!(
338            serde_json::to_value(&ns_response).expect("Serialization failed"),
339            json
340        );
341
342        // Without properties
343        let json_no_props = serde_json::json!({
344            "namespace": ["db", "schema"]
345        });
346        let ns_response_no_props: NamespaceResponse =
347            serde_json::from_value(json_no_props.clone()).expect("Deserialization failed");
348        assert_eq!(ns_response_no_props, NamespaceResponse {
349            namespace: NamespaceIdent::from_vec(vec!["db".to_string(), "schema".to_string()])
350                .unwrap(),
351            properties: HashMap::new(),
352        });
353        assert_eq!(
354            serde_json::to_value(&ns_response_no_props).expect("Serialization failed"),
355            json_no_props
356        );
357    }
358}