Skip to main content

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    #[serde(skip_serializing_if = "Option::is_none")]
255    pub location: Option<String>,
256    /// Table schema
257    pub schema: Schema,
258    /// Optional partition specification. If not provided, the table will be unpartitioned.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub partition_spec: Option<UnboundPartitionSpec>,
261    /// Optional sort order for the table
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub write_order: Option<SortOrder>,
264    /// Whether to stage the create for a transaction (true) or create immediately (false)
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub stage_create: Option<bool>,
267    /// Optional properties to set on the table
268    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
269    pub properties: HashMap<String, String>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
273/// Request to commit updates to a table.
274///
275/// Commits have two parts: requirements and updates. Requirements are assertions that will
276/// be validated before attempting to make and commit changes. Updates are changes to make
277/// to table metadata.
278///
279/// Create table transactions that are started by createTable with `stage-create` set to true
280/// are committed using this request. Transactions should include all changes to the table,
281/// including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate.
282pub struct CommitTableRequest {
283    /// Table identifier to update; must be present for CommitTransactionRequest
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub identifier: Option<TableIdent>,
286    /// List of requirements that must be satisfied before committing changes
287    pub requirements: Vec<TableRequirement>,
288    /// List of updates to apply to the table metadata
289    pub updates: Vec<TableUpdate>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293#[serde(rename_all = "kebab-case")]
294/// Response returned when a table is successfully updated.
295///
296/// The table metadata JSON is returned in the metadata field. The corresponding file location
297/// of table metadata must be returned in the metadata-location field. Clients can check whether
298/// metadata has changed by comparing metadata locations.
299pub struct CommitTableResponse {
300    /// Location of the updated table metadata file
301    pub metadata_location: String,
302    /// The table's updated metadata
303    pub metadata: TableMetadata,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
307#[serde(rename_all = "kebab-case")]
308/// Request to register a table using an existing metadata file location.
309pub struct RegisterTableRequest {
310    /// Name of the table to register
311    pub name: String,
312    /// Location of the metadata file for the table
313    pub metadata_location: String,
314    /// Whether to overwrite table metadata if the table already exists
315    pub overwrite: Option<bool>,
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_namespace_response_serde() {
324        let json = serde_json::json!({
325            "namespace": ["nested", "ns"],
326            "properties": {
327                "key1": "value1",
328                "key2": "value2"
329            }
330        });
331        let ns_response: NamespaceResponse =
332            serde_json::from_value(json.clone()).expect("Deserialization failed");
333        assert_eq!(ns_response, NamespaceResponse {
334            namespace: NamespaceIdent::from_vec(vec!["nested".to_string(), "ns".to_string()])
335                .unwrap(),
336            properties: HashMap::from([
337                ("key1".to_string(), "value1".to_string()),
338                ("key2".to_string(), "value2".to_string()),
339            ]),
340        });
341        assert_eq!(
342            serde_json::to_value(&ns_response).expect("Serialization failed"),
343            json
344        );
345
346        // Without properties
347        let json_no_props = serde_json::json!({
348            "namespace": ["db", "schema"]
349        });
350        let ns_response_no_props: NamespaceResponse =
351            serde_json::from_value(json_no_props.clone()).expect("Deserialization failed");
352        assert_eq!(ns_response_no_props, NamespaceResponse {
353            namespace: NamespaceIdent::from_vec(vec!["db".to_string(), "schema".to_string()])
354                .unwrap(),
355            properties: HashMap::new(),
356        });
357        assert_eq!(
358            serde_json::to_value(&ns_response_no_props).expect("Serialization failed"),
359            json_no_props
360        );
361    }
362
363    fn test_create_table_request_schema() -> Schema {
364        serde_json::from_value(serde_json::json!({
365            "type": "struct",
366            "schema-id": 1,
367            "fields": [
368                {
369                    "id": 1,
370                    "name": "foo",
371                    "required": false,
372                    "type": "string"
373                },
374                {
375                    "id": 2,
376                    "name": "bar",
377                    "required": true,
378                    "type": "int"
379                }
380            ],
381            "identifier-field-ids": [2]
382        }))
383        .expect("Failed to deserialize test schema")
384    }
385
386    #[test]
387    fn test_create_table_request_minimal_serialization() {
388        let request = CreateTableRequest {
389            name: "tbl1".to_string(),
390            location: None,
391            schema: test_create_table_request_schema(),
392            partition_spec: None,
393            write_order: None,
394            stage_create: None,
395            properties: HashMap::new(),
396        };
397
398        let serialized = serde_json::to_value(&request).expect("Serialization failed");
399        let object = serialized.as_object().expect("Expected a JSON object");
400        assert!(object.contains_key("name"));
401        assert!(object.contains_key("schema"));
402        assert!(!object.contains_key("location"));
403        assert!(!object.contains_key("partition-spec"));
404        assert!(!object.contains_key("write-order"));
405        assert!(!object.contains_key("stage-create"));
406        assert!(!object.contains_key("properties"));
407    }
408
409    #[test]
410    fn test_create_table_request_full_serialization() {
411        let request: CreateTableRequest = serde_json::from_value(serde_json::json!({
412            "name": "tbl1",
413            "location": "s3://warehouse/tbl1",
414            "schema": test_create_table_request_schema(),
415            "partition-spec": {
416                "spec-id": 1,
417                "fields": [
418                    {
419                        "source-id": 2,
420                        "field-id": 1000,
421                        "name": "bar",
422                        "transform": "identity"
423                    }
424                ]
425            },
426            "write-order": {
427                "order-id": 1,
428                "fields": [
429                    {
430                        "transform": "identity",
431                        "source-id": 2,
432                        "direction": "asc",
433                        "null-order": "nulls-first"
434                    }
435                ]
436            },
437            "stage-create": true,
438            "properties": {
439                "owner": "test"
440            }
441        }))
442        .expect("Deserialization failed");
443
444        let serialized = serde_json::to_value(&request).expect("Serialization failed");
445        let object = serialized.as_object().expect("Expected a JSON object");
446        assert_eq!(
447            object.get("location"),
448            Some(&serde_json::json!("s3://warehouse/tbl1"))
449        );
450        assert!(object.contains_key("partition-spec"));
451        assert!(object.contains_key("write-order"));
452        assert_eq!(object.get("stage-create"), Some(&serde_json::json!(true)));
453        assert!(object.contains_key("properties"));
454    }
455
456    #[test]
457    fn test_create_table_request_deserialize_explicit_nulls() {
458        let request: CreateTableRequest = serde_json::from_value(serde_json::json!({
459            "name": "tbl1",
460            "location": null,
461            "schema": test_create_table_request_schema(),
462            "partition-spec": null,
463            "write-order": null,
464            "stage-create": null
465        }))
466        .expect("Deserialization failed");
467
468        assert_eq!(request.name, "tbl1");
469        assert_eq!(request.location, None);
470        assert_eq!(request.partition_spec, None);
471        assert_eq!(request.write_order, None);
472        assert_eq!(request.stage_create, None);
473        assert!(request.properties.is_empty());
474    }
475}