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}